func TestIgnitionHandler_V2JSON(t *testing.T) { content := `{"ignition":{"version":"2.0.0","config":{}},"storage":{},"systemd":{"units":[{"name":"etcd2.service","enable":true},{"name":"a1b2c3d4.service","enable":true}]},"networkd":{},"passwd":{}}` profile := &storagepb.Profile{ Id: fake.Group.Profile, IgnitionId: "file.ign", } store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: profile}, IgnitionConfigs: map[string]string{"file.ign": content}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.ignitionHandler(c) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Ignition template is rendered with Group metadata and selectors // - Rendered Ignition template is parsed as JSON // - Ignition Config served as JSON assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType)) assert.Equal(t, expectedIgnitionV2, w.Body.String()) }
func TestCloudHandler(t *testing.T) { content := `#cloud-config coreos: etcd2: name: {{.uuid}} units: - name: {{.service_name}} ` expected := `#cloud-config coreos: etcd2: name: a1b2c3d4 units: - name: etcd2 ` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, CloudConfigs: map[string]string{fake.Profile.CloudId: content}, } srv := server.NewServer(&server.Config{Store: store}) h := cloudHandler(srv) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Cloud config is rendered with Group metadata and selectors assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, expected, w.Body.String()) }
func TestIgnitionHandler_V2YAML(t *testing.T) { content := ` systemd: units: - name: {{.service_name}}.service enable: true - name: {{.uuid}}.service enable: true ` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: testProfileIgnitionYAML}, IgnitionConfigs: map[string]string{testProfileIgnitionYAML.IgnitionId: content}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.ignitionHandler(c) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Ignition template is rendered with Group metadata and selectors // - Rendered Ignition template is parsed as YAML // - Ignition Config served as JSON assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType)) assert.Equal(t, expectedIgnitionV2, w.Body.String()) }
func TestIgnitionHandler_MissingTemplateMetadata(t *testing.T) { content := ` ignition_version: 1 systemd: units: - name: {{.missing_key}} enable: true ` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, IgnitionConfigs: map[string]string{fake.Profile.IgnitionId: content}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.ignitionHandler(c) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Ignition template rendering errors because "missing_key" is not // present in the Group metadata assert.Equal(t, http.StatusNotFound, w.Code) }
func TestGenericHandler(t *testing.T) { content := `#foo-bar-baz template UUID={{.uuid}} SERVICE={{.service_name}} ` expected := `#foo-bar-baz template UUID=a1b2c3d4 SERVICE=etcd2 ` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, GenericConfigs: map[string]string{fake.Profile.GenericId: content}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.genericHandler(c) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Generic config is rendered with Group metadata and selectors assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, expected, w.Body.String()) }
// HTTPHandler returns a HTTP handler for the server. func (s *Server) HTTPHandler() http.Handler { mux := http.NewServeMux() srv := server.NewServer(&server.Config{s.store}) // bootcfg version mux.Handle("/", logRequests(versionHandler())) // Boot via GRUB mux.Handle("/grub", logRequests(NewHandler(selectProfile(srv, grubHandler())))) // Boot via iPXE mux.Handle("/boot.ipxe", logRequests(ipxeInspect())) mux.Handle("/boot.ipxe.0", logRequests(ipxeInspect())) mux.Handle("/ipxe", logRequests(NewHandler(selectProfile(srv, ipxeHandler())))) // Boot via Pixiecore mux.Handle("/pixiecore/v1/boot/", logRequests(NewHandler(pixiecoreHandler(srv)))) // Ignition Config mux.Handle("/ignition", logRequests(NewHandler(selectGroup(srv, ignitionHandler(srv))))) // Cloud-Config mux.Handle("/cloud", logRequests(NewHandler(selectGroup(srv, cloudHandler(srv))))) // Generic template mux.Handle("/generic", logRequests(NewHandler(selectGroup(srv, genericHandler(srv))))) // metadata mux.Handle("/metadata", logRequests(NewHandler(selectGroup(srv, metadataHandler())))) // Signatures if s.signer != nil { signerChain := func(next http.Handler) http.Handler { return logRequests(sign.SignatureHandler(s.signer, next)) } mux.Handle("/grub.sig", signerChain(NewHandler(selectProfile(srv, grubHandler())))) mux.Handle("/boot.ipxe.sig", signerChain(ipxeInspect())) mux.Handle("/boot.ipxe.0.sig", signerChain(ipxeInspect())) mux.Handle("/ipxe.sig", signerChain(NewHandler(selectProfile(srv, ipxeHandler())))) mux.Handle("/pixiecore/v1/boot.sig/", signerChain(NewHandler(pixiecoreHandler(srv)))) mux.Handle("/ignition.sig", signerChain(NewHandler(selectGroup(srv, ignitionHandler(srv))))) mux.Handle("/cloud.sig", signerChain(NewHandler(selectGroup(srv, cloudHandler(srv))))) mux.Handle("/generic.sig", signerChain(NewHandler(selectGroup(srv, genericHandler(srv))))) mux.Handle("/metadata.sig", signerChain(NewHandler(selectGroup(srv, metadataHandler())))) } if s.armoredSigner != nil { signerChain := func(next http.Handler) http.Handler { return logRequests(sign.SignatureHandler(s.armoredSigner, next)) } mux.Handle("/grub.asc", signerChain(NewHandler(selectProfile(srv, grubHandler())))) mux.Handle("/boot.ipxe.asc", signerChain(ipxeInspect())) mux.Handle("/boot.ipxe.0.asc", signerChain(ipxeInspect())) mux.Handle("/ipxe.asc", signerChain(NewHandler(selectProfile(srv, ipxeHandler())))) mux.Handle("/pixiecore/v1/boot.asc/", signerChain(NewHandler(pixiecoreHandler(srv)))) mux.Handle("/ignition.asc", signerChain(NewHandler(selectGroup(srv, ignitionHandler(srv))))) mux.Handle("/cloud.asc", signerChain(NewHandler(selectGroup(srv, cloudHandler(srv))))) mux.Handle("/generic.asc", signerChain(NewHandler(selectGroup(srv, genericHandler(srv))))) mux.Handle("/metadata.asc", signerChain(NewHandler(selectGroup(srv, metadataHandler())))) } // kernel, initrd, and TLS assets if s.assetsPath != "" { mux.Handle("/assets/", logRequests(http.StripPrefix("/assets/", http.FileServer(http.Dir(s.assetsPath))))) } return mux }
func TestCloudHandler_MissingCtxProfile(t *testing.T) { srv := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) h := cloudHandler(srv) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusNotFound, w.Code) }
func TestIgnitionHandler_MissingIgnitionConfig(t *testing.T) { srv := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) h := ignitionHandler(srv) ctx := withProfile(context.Background(), fake.Profile) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) assert.Equal(t, http.StatusNotFound, w.Code) }
func TestIgnitionHandler_MissingCtxProfile(t *testing.T) { logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) h := srv.ignitionHandler(c) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusNotFound, w.Code) }
func TestPixiecoreHandler_NoMatchingGroup(t *testing.T) { logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) h := srv.pixiecoreHandler(c) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/"+validMACStr, nil) h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusNotFound, w.Code) }
func TestPixiecoreHandler_InvalidMACAddress(t *testing.T) { logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: &fake.EmptyStore{}}) h := srv.pixiecoreHandler(c) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Equal(t, "invalid MAC address /\n", w.Body.String()) }
func TestPixiecoreHandler_NoMatchingProfile(t *testing.T) { store := &fake.FixedStore{ Groups: map[string]*storagepb.Group{fake.Group.Id: fake.Group}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.pixiecoreHandler(c) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/"+validMACStr, nil) h.ServeHTTP(context.Background(), w, req) assert.Equal(t, http.StatusNotFound, w.Code) }
func TestIgnitionHandler_V1JSON(t *testing.T) { content := `{"ignitionVersion": 1,"systemd":{"units":[{"name":"{{.service_name}}.service","enable":true},{"name":"{{.uuid}}.service","enable":true}]}}` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, IgnitionConfigs: map[string]string{fake.Profile.IgnitionId: content}, } srv := server.NewServer(&server.Config{Store: store}) h := ignitionHandler(srv) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Ignition template is rendered with Group metadata and selectors // - Rendered Ignition template is parsed as JSON // - Ignition Config served as JSON assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType)) assert.Equal(t, expectedIgnitionV1, w.Body.String()) }
func TestPixiecoreHandler(t *testing.T) { store := &fake.FixedStore{ Groups: map[string]*storagepb.Group{testGroupWithMAC.Id: testGroupWithMAC}, Profiles: map[string]*storagepb.Profile{testGroupWithMAC.Profile: fake.Profile}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.pixiecoreHandler(c) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/"+validMACStr, nil) h.ServeHTTP(context.Background(), w, req) // assert that: // - MAC address parameter is used for Group matching // - the Profile's NetBoot config is rendered as Pixiecore JSON expectedJSON := `{"kernel":"/image/kernel","initrd":["/image/initrd_a","/image/initrd_b"],"cmdline":{"a":"b","c":""}}` assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, jsonContentType, w.HeaderMap.Get(contentType)) assert.Equal(t, expectedJSON, w.Body.String()) }
func TestCloudHandler_MissingTemplateMetadata(t *testing.T) { content := `#cloud-config coreos: etcd2: name: {{.missing_key}} ` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, CloudConfigs: map[string]string{fake.Profile.CloudId: content}, } srv := server.NewServer(&server.Config{Store: store}) h := cloudHandler(srv) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Cloud-config template rendering errors because "missing_key" is not // present in the Group metadata assert.Equal(t, http.StatusNotFound, w.Code) }
func TestGenericHandler_MissingTemplateMetadata(t *testing.T) { content := `#foo-bar-baz template KEY={{.missing_key}} ` store := &fake.FixedStore{ Profiles: map[string]*storagepb.Profile{fake.Group.Profile: fake.Profile}, GenericConfigs: map[string]string{fake.Profile.GenericId: content}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) h := srv.cloudHandler(c) ctx := withGroup(context.Background(), fake.Group) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/", nil) h.ServeHTTP(ctx, w, req) // assert that: // - Generic template rendering errors because "missing_key" is not // present in the Group metadata assert.Equal(t, http.StatusNotFound, w.Code) }
func TestSelectGroup(t *testing.T) { store := &fake.FixedStore{ Groups: map[string]*storagepb.Group{fake.Group.Id: fake.Group}, } logger, _ := logtest.NewNullLogger() srv := NewServer(&Config{Logger: logger}) c := server.NewServer(&server.Config{Store: store}) next := func(ctx context.Context, w http.ResponseWriter, req *http.Request) { group, err := groupFromContext(ctx) assert.Nil(t, err) assert.Equal(t, fake.Group, group) fmt.Fprintf(w, "next handler called") } // assert that: // - query params are used to match uuid=a1b2c3d4 to fake.Group // - the fake.Group is added to the context // - next handler is called h := srv.selectGroup(c, ContextHandlerFunc(next)) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "?uuid=a1b2c3d4", nil) h.ServeHTTP(context.Background(), w, req) assert.Equal(t, "next handler called", w.Body.String()) }
func main() { flags := struct { address string rpcAddress string dataPath string assetsPath string logLevel string certFile string keyFile string caFile string keyRingPath string version bool help bool }{} flag.StringVar(&flags.address, "address", "127.0.0.1:8080", "HTTP listen address") flag.StringVar(&flags.rpcAddress, "rpc-address", "", "RPC listen address") flag.StringVar(&flags.dataPath, "data-path", "/var/lib/bootcfg", "Path to data directory") flag.StringVar(&flags.assetsPath, "assets-path", "/var/lib/bootcfg/assets", "Path to static assets") // Log levels https://godoc.org/github.com/coreos/pkg/capnslog#LogLevel flag.StringVar(&flags.logLevel, "log-level", "info", "Set the logging level") // gRPC Server TLS flag.StringVar(&flags.certFile, "cert-file", "/etc/bootcfg/server.crt", "Path to the server TLS certificate file") flag.StringVar(&flags.keyFile, "key-file", "/etc/bootcfg/server.key", "Path to the server TLS key file") // TLS Client Authentication flag.StringVar(&flags.caFile, "ca-file", "/etc/bootcfg/ca.crt", "Path to the CA verify and authenticate client certificates") // Signing flag.StringVar(&flags.keyRingPath, "key-ring-path", "", "Path to a private keyring file") // subcommands flag.BoolVar(&flags.version, "version", false, "print version and exit") flag.BoolVar(&flags.help, "help", false, "print usage and exit") // parse command-line and environment variable arguments flag.Parse() if err := flagutil.SetFlagsFromEnv(flag.CommandLine, "BOOTCFG"); err != nil { log.Fatal(err.Error()) } // restrict OpenPGP passphrase to pass via environment variable only passphrase := os.Getenv("BOOTCFG_PASSPHRASE") if flags.version { fmt.Println(version.Version) return } if flags.help { flag.Usage() return } // validate arguments if url, err := url.Parse(flags.address); err != nil || url.String() == "" { log.Fatal("A valid HTTP listen address is required") } if finfo, err := os.Stat(flags.dataPath); err != nil || !finfo.IsDir() { log.Fatal("A valid -data-path is required") } if flags.assetsPath != "" { if finfo, err := os.Stat(flags.assetsPath); err != nil || !finfo.IsDir() { log.Fatalf("Provide a valid -assets-path or '' to disable asset serving: %s", flags.assetsPath) } } if flags.rpcAddress != "" { if _, err := os.Stat(flags.certFile); err != nil { log.Fatalf("Provide a valid TLS server certificate with -cert-file: %v", err) } if _, err := os.Stat(flags.keyFile); err != nil { log.Fatalf("Provide a valid TLS server key with -key-file: %v", err) } if _, err := os.Stat(flags.caFile); err != nil { log.Fatalf("Provide a valid TLS certificate authority for authorizing client certificates: %v", err) } } // logging setup lvl, err := capnslog.ParseLevel(strings.ToUpper(flags.logLevel)) if err != nil { log.Fatalf("invalid log-level: %v", err) } capnslog.SetGlobalLogLevel(lvl) capnslog.SetFormatter(capnslog.NewPrettyFormatter(os.Stdout, false)) // (optional) signing var signer, armoredSigner sign.Signer if flags.keyRingPath != "" { entity, err := sign.LoadGPGEntity(flags.keyRingPath, passphrase) if err != nil { log.Fatal(err) } signer = sign.NewGPGSigner(entity) armoredSigner = sign.NewArmoredGPGSigner(entity) } // storage store := storage.NewFileStore(&storage.Config{ Root: flags.dataPath, }) // core logic server := server.NewServer(&server.Config{ Store: store, }) // gRPC Server (feature disabled by default) if flags.rpcAddress != "" { log.Infof("starting bootcfg gRPC server on %s", flags.rpcAddress) log.Infof("Using TLS server certificate: %s", flags.certFile) log.Infof("Using TLS server key: %s", flags.keyFile) log.Infof("Using CA certificate: %s to authenticate client certificates", flags.caFile) lis, err := net.Listen("tcp", flags.rpcAddress) if err != nil { log.Fatalf("failed to start listening: %v", err) } tlsinfo := tlsutil.TLSInfo{ CertFile: flags.certFile, KeyFile: flags.keyFile, CAFile: flags.caFile, } tlscfg, err := tlsinfo.ServerConfig() if err != nil { log.Fatalf("Invalid TLS credentials: %v", err) } grpcServer := rpc.NewServer(server, tlscfg) go grpcServer.Serve(lis) defer grpcServer.Stop() } // HTTP Server config := &web.Config{ Store: store, AssetsPath: flags.assetsPath, Signer: signer, ArmoredSigner: armoredSigner, } httpServer := web.NewServer(config) log.Infof("starting bootcfg HTTP server on %s", flags.address) err = http.ListenAndServe(flags.address, httpServer.HTTPHandler()) if err != nil { log.Fatalf("failed to start listening: %v", err) } }