diff --git a/.gitignore b/.gitignore index 070d0c1..1b897b9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ tests/e2e/configuration.yaml # TLS certificate/key *.crt *.key +*.pem # trivy artifacts .trivy diff --git a/README.md b/README.md index 77fa1d0..9575a6a 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Once configured, all requests to the Nomad API automatically contain a `X-Nomad- ### TLS -We highly encourage the use of TLS in this API to increase the security. To enable TLS, set `server.tls` or the corresponding environment variable to true and specify the `server.certfile` and `server.keyfile` options. +We highly encourage the use of TLS in this API to increase the security. To enable TLS, set `server.tls.active` or the corresponding environment variable to true and specify the `server.tls.certfile` and `server.tls.keyfile` options. You can create a self-signed certificate to use with this API using the following command. diff --git a/cmd/poseidon/main.go b/cmd/poseidon/main.go index e577b87..426db48 100644 --- a/cmd/poseidon/main.go +++ b/cmd/poseidon/main.go @@ -24,13 +24,13 @@ var ( func runServer(server *http.Server) { log.WithField("address", server.Addr).Info("Starting server") var err error - if config.Config.Server.TLS { + if config.Config.Server.TLS.Active { server.TLSConfig = config.TLSConfig log. - WithField("CertFile", config.Config.Server.CertFile). - WithField("KeyFile", config.Config.Server.KeyFile). + WithField("CertFile", config.Config.Server.TLS.CertFile). + WithField("KeyFile", config.Config.Server.TLS.KeyFile). Debug("Using TLS") - err = server.ListenAndServeTLS(config.Config.Server.CertFile, config.Config.Server.KeyFile) + err = server.ListenAndServeTLS(config.Config.Server.TLS.CertFile, config.Config.Server.TLS.KeyFile) } else { err = server.ListenAndServe() } @@ -45,20 +45,16 @@ func runServer(server *http.Server) { func initServer() *http.Server { // API initialization - nomadAPIClient, err := nomad.NewExecutorAPI( - config.Config.NomadAPIURL(), - config.Config.Nomad.Namespace, - config.Config.Nomad.Token, - ) + nomadAPIClient, err := nomad.NewExecutorAPI(&config.Config.Nomad) if err != nil { - log.WithError(err).WithField("nomad url", config.Config.NomadAPIURL()).Fatal("Error parsing the nomad url") + log.WithError(err).WithField("nomad config", config.Config.Nomad).Fatal("Error creating Nomad API client") } runnerManager := runner.NewNomadRunnerManager(nomadAPIClient, context.Background()) environmentManager := environment.NewNomadEnvironmentManager(runnerManager, nomadAPIClient) return &http.Server{ - Addr: config.Config.PoseidonAPIURL().Host, + Addr: config.Config.Server.URL().Host, WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, IdleTimeout: time.Second * 60, diff --git a/configuration.example.yaml b/configuration.example.yaml index caca1b1..6a6e014 100644 --- a/configuration.example.yaml +++ b/configuration.example.yaml @@ -6,12 +6,14 @@ server: port: 7200 # If set, this token is required in the X-Poseidon-Token header for each route except /health token: SECRET - # If set, the API uses TLS for all incoming connections - tls: true - # The path to the certificate file used for TLS - certfile: ./poseidon.crt - # The path to the key file used for TLS - keyfile: ./poseidon.key + # Configuration of TLS between the web client and Poseidon. + tls: + # If set, the API uses TLS for all incoming connections. + active: true + # The path to the certificate file used for TLS + certfile: ./poseidon.crt + # The path to the key file used for TLS + keyfile: ./poseidon.key # If true, an additional WebSocket connection will be opened to split stdout and stderr when executing interactively interactiveStderr: true @@ -23,8 +25,16 @@ nomad: port: 4646 # Authenticate requests to the Nomad server with this token token: SECRET - # Specifies whether to use TLS when communicating with the Nomad server - tls: false + # Configuration of TLS between the Poseidon and Nomad. + tls: + # Specifies whether to use TLS when communicating with the Nomad server. + active: false + # The path to the certificate of the CA authority of the Nomad host. + cafile: ./ca.crt + # The path to the client certificate file used for TLS + certfile: ./poseidon.crt + # The path to the client key file used for TLS + keyfile: ./poseidon.key # Nomad namespace to use. If unset, 'default' is used namespace: poseidon diff --git a/internal/api/runners.go b/internal/api/runners.go index 80c739d..a1ddcf4 100644 --- a/internal/api/runners.go +++ b/internal/api/runners.go @@ -93,7 +93,7 @@ func (r *RunnerController) execute(writer http.ResponseWriter, request *http.Req } var scheme string - if config.Config.Server.TLS { + if config.Config.Server.TLS.Active { scheme = "wss" } else { scheme = "ws" diff --git a/internal/config/config.go b/internal/config/config.go index 634db4c..359b5ca 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,19 +19,26 @@ import ( var ( Config = &configuration{ Server: server{ - Address: "127.0.0.1", - Port: 7200, - Token: "", - TLS: false, - CertFile: "", - KeyFile: "", + Address: "127.0.0.1", + Port: 7200, + Token: "", + TLS: TLS{ + Active: false, + CertFile: "", + KeyFile: "", + }, InteractiveStderr: true, }, - Nomad: nomad{ - Address: "127.0.0.1", - Port: 4646, - Token: "", - TLS: false, + Nomad: Nomad{ + Address: "127.0.0.1", + Port: 4646, + Token: "", + TLS: TLS{ + Active: false, + CAFile: "", + CertFile: "", + KeyFile: "", + }, Namespace: "default", }, Logger: logger{ @@ -54,21 +61,37 @@ type server struct { Address string Port int Token string - TLS bool - CertFile string - KeyFile string + TLS TLS InteractiveStderr bool } -// nomad configures the used Nomad cluster. -type nomad struct { +// URL returns the URL of the Poseidon webserver. +func (s *server) URL() *url.URL { + return parseURL(s.Address, s.Port, s.TLS.Active) +} + +// Nomad configures the used Nomad cluster. +type Nomad struct { Address string Port int Token string - TLS bool + TLS TLS Namespace string } +// URL returns the URL for the configured Nomad cluster. +func (n *Nomad) URL() *url.URL { + return parseURL(n.Address, n.Port, n.TLS.Active) +} + +// TLS configures TLS on a connection. +type TLS struct { + Active bool + CAFile string + CertFile string + KeyFile string +} + // logger configures the used logger. type logger struct { Level string @@ -77,7 +100,7 @@ type logger struct { // configuration contains the complete configuration of Poseidon. type configuration struct { Server server - Nomad nomad + Nomad Nomad Logger logger } @@ -96,16 +119,6 @@ func InitConfig() error { return nil } -// NomadAPIURL returns the URL for the configured Nomad cluster. -func (c *configuration) NomadAPIURL() *url.URL { - return parseURL(Config.Nomad.Address, Config.Nomad.Port, Config.Nomad.TLS) -} - -// PoseidonAPIURL returns the URL of the Poseidon webserver. -func (c *configuration) PoseidonAPIURL() *url.URL { - return parseURL(Config.Server.Address, Config.Server.Port, false) -} - func parseURL(address string, port int, tlsEnabled bool) *url.URL { scheme := "http" if tlsEnabled { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4366cd9..726490d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -13,9 +13,9 @@ import ( ) var ( - getServerPort = func(c *configuration) interface{} { return c.Server.Port } - getNomadToken = func(c *configuration) interface{} { return c.Nomad.Token } - getNomadTLS = func(c *configuration) interface{} { return c.Nomad.TLS } + getServerPort = func(c *configuration) interface{} { return c.Server.Port } + getNomadToken = func(c *configuration) interface{} { return c.Nomad.Token } + getNomadTLSActive = func(c *configuration) interface{} { return c.Nomad.TLS.Active } ) func newTestConfiguration() *configuration { @@ -24,11 +24,13 @@ func newTestConfiguration() *configuration { Address: "127.0.0.1", Port: 3000, }, - Nomad: nomad{ + Nomad: Nomad{ Address: "127.0.0.2", Port: 4646, Token: "SECRET", - TLS: false, + TLS: TLS{ + Active: false, + }, }, Logger: logger{ Level: "INFO", @@ -87,8 +89,8 @@ func TestReadEnvironmentVariables(t *testing.T) { {"SERVER_PORT", "4000", 4000, getServerPort}, {"SERVER_PORT", "hello", 3000, getServerPort}, {"NOMAD_TOKEN", "ACCESS", "ACCESS", getNomadToken}, - {"NOMAD_TLS", "true", true, getNomadTLS}, - {"NOMAD_TLS", "hello", false, getNomadTLS}, + {"NOMAD_TLS_ACTIVE", "true", true, getNomadTLSActive}, + {"NOMAD_TLS_ACTIVE", "hello", false, getNomadTLSActive}, } prefix := "POSEIDON_TEST" for _, testCase := range environmentTests { @@ -131,8 +133,8 @@ func TestReadYamlConfigFile(t *testing.T) { }{ {[]byte("server:\n port: 5000\n"), 5000, getServerPort}, {[]byte("nomad:\n token: ACCESS\n"), "ACCESS", getNomadToken}, - {[]byte("nomad:\n tls: true\n"), true, getNomadTLS}, - {[]byte(""), false, getNomadTLS}, + {[]byte("nomad:\n tls:\n active: true\n"), true, getNomadTLSActive}, + {[]byte(""), false, getNomadTLSActive}, {[]byte("nomad:\n token:\n"), "SECRET", getNomadToken}, } for _, testCase := range yamlTests { @@ -197,12 +199,12 @@ func TestURLParsing(t *testing.T) { func TestNomadAPIURL(t *testing.T) { config := newTestConfiguration() - assert.Equal(t, "http", config.NomadAPIURL().Scheme) - assert.Equal(t, "127.0.0.2:4646", config.NomadAPIURL().Host) + assert.Equal(t, "http", config.Nomad.URL().Scheme) + assert.Equal(t, "127.0.0.2:4646", config.Nomad.URL().Host) } func TestPoseidonAPIURL(t *testing.T) { config := newTestConfiguration() - assert.Equal(t, "http", config.PoseidonAPIURL().Scheme) - assert.Equal(t, "127.0.0.1:3000", config.PoseidonAPIURL().Host) + assert.Equal(t, "http", config.Server.URL().Scheme) + assert.Equal(t, "127.0.0.1:3000", config.Server.URL().Host) } diff --git a/internal/nomad/api_querier.go b/internal/nomad/api_querier.go index 6e5b3fc..d8dd11d 100644 --- a/internal/nomad/api_querier.go +++ b/internal/nomad/api_querier.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" nomadApi "github.com/hashicorp/nomad/api" + "gitlab.hpi.de/codeocean/codemoon/poseidon/internal/config" "io" - "net/url" ) var ( @@ -16,7 +16,7 @@ var ( // apiQuerier provides access to the Nomad functionality. type apiQuerier interface { // init prepares an apiClient to be able to communicate to a provided Nomad API. - init(nomadURL *url.URL, nomadNamespace, nomadToken string) (err error) + init(nomadConfig *config.Nomad) (err error) // LoadJobList loads the list of jobs from the Nomad API. LoadJobList() (list []*nomadApi.JobListStub, err error) @@ -61,17 +61,24 @@ type nomadAPIClient struct { namespace string } -func (nc *nomadAPIClient) init(nomadURL *url.URL, nomadNamespace, nomadToken string) (err error) { +func (nc *nomadAPIClient) init(nomadConfig *config.Nomad) (err error) { + nomadTLSConfig := &nomadApi.TLSConfig{} + if nomadConfig.TLS.Active { + nomadTLSConfig.CACert = nomadConfig.TLS.CAFile + nomadTLSConfig.ClientCert = nomadConfig.TLS.CertFile + nomadTLSConfig.ClientKey = nomadConfig.TLS.KeyFile + } + nc.client, err = nomadApi.NewClient(&nomadApi.Config{ - Address: nomadURL.String(), - TLSConfig: &nomadApi.TLSConfig{}, - Namespace: nomadNamespace, - SecretID: nomadToken, + Address: nomadConfig.URL().String(), + TLSConfig: nomadTLSConfig, + Namespace: nomadConfig.Namespace, + SecretID: nomadConfig.Token, }) if err != nil { return fmt.Errorf("error creating new Nomad client: %w", err) } - nc.namespace = nomadNamespace + nc.namespace = nomadConfig.Namespace return nil } diff --git a/internal/nomad/api_querier_mock.go b/internal/nomad/api_querier_mock.go index faa8d16..b7817d6 100644 --- a/internal/nomad/api_querier_mock.go +++ b/internal/nomad/api_querier_mock.go @@ -6,12 +6,11 @@ import ( context "context" api "github.com/hashicorp/nomad/api" + config "gitlab.hpi.de/codeocean/codemoon/poseidon/internal/config" io "io" mock "github.com/stretchr/testify/mock" - - url "net/url" ) // apiQuerierMock is an autogenerated mock type for the apiQuerier type @@ -202,13 +201,13 @@ func (_m *apiQuerierMock) allocation(jobID string) (*api.Allocation, error) { return r0, r1 } -// init provides a mock function with given fields: nomadURL, nomadNamespace, nomadToken -func (_m *apiQuerierMock) init(nomadURL *url.URL, nomadNamespace string, nomadToken string) error { - ret := _m.Called(nomadURL, nomadNamespace, nomadToken) +// init provides a mock function with given fields: nomadConfig +func (_m *apiQuerierMock) init(nomadConfig *config.Nomad) error { + ret := _m.Called(nomadConfig) var r0 error - if rf, ok := ret.Get(0).(func(*url.URL, string, string) error); ok { - r0 = rf(nomadURL, nomadNamespace, nomadToken) + if rf, ok := ret.Get(0).(func(*config.Nomad) error); ok { + r0 = rf(nomadConfig) } else { r0 = ret.Error(0) } diff --git a/internal/nomad/executor_api_mock.go b/internal/nomad/executor_api_mock.go index 17e5a5a..58a06db 100644 --- a/internal/nomad/executor_api_mock.go +++ b/internal/nomad/executor_api_mock.go @@ -6,12 +6,11 @@ import ( context "context" api "github.com/hashicorp/nomad/api" + config "gitlab.hpi.de/codeocean/codemoon/poseidon/internal/config" io "io" mock "github.com/stretchr/testify/mock" - - url "net/url" ) // ExecutorAPIMock is an autogenerated mock type for the ExecutorAPI type @@ -394,13 +393,13 @@ func (_m *ExecutorAPIMock) allocation(jobID string) (*api.Allocation, error) { return r0, r1 } -// init provides a mock function with given fields: nomadURL, nomadNamespace, nomadToken -func (_m *ExecutorAPIMock) init(nomadURL *url.URL, nomadNamespace string, nomadToken string) error { - ret := _m.Called(nomadURL, nomadNamespace, nomadToken) +// init provides a mock function with given fields: nomadConfig +func (_m *ExecutorAPIMock) init(nomadConfig *config.Nomad) error { + ret := _m.Called(nomadConfig) var r0 error - if rf, ok := ret.Get(0).(func(*url.URL, string, string) error); ok { - r0 = rf(nomadURL, nomadNamespace, nomadToken) + if rf, ok := ret.Get(0).(func(*config.Nomad) error); ok { + r0 = rf(nomadConfig) } else { r0 = ret.Error(0) } diff --git a/internal/nomad/nomad.go b/internal/nomad/nomad.go index a56c007..533437b 100644 --- a/internal/nomad/nomad.go +++ b/internal/nomad/nomad.go @@ -10,7 +10,6 @@ import ( "gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/logging" "gitlab.hpi.de/codeocean/codemoon/poseidon/pkg/nullio" "io" - "net/url" "strconv" "time" ) @@ -81,15 +80,15 @@ type APIClient struct { // NewExecutorAPI creates a new api client. // One client is usually sufficient for the complete runtime of the API. -func NewExecutorAPI(nomadURL *url.URL, nomadNamespace, nomadToken string) (ExecutorAPI, error) { +func NewExecutorAPI(nomadConfig *config.Nomad) (ExecutorAPI, error) { client := &APIClient{apiQuerier: &nomadAPIClient{}} - err := client.init(nomadURL, nomadNamespace, nomadToken) + err := client.init(nomadConfig) return client, err } // init prepares an apiClient to be able to communicate to a provided Nomad API. -func (a *APIClient) init(nomadURL *url.URL, nomadNamespace, nomadToken string) error { - if err := a.apiQuerier.init(nomadURL, nomadNamespace, nomadToken); err != nil { +func (a *APIClient) init(nomadConfig *config.Nomad) error { + if err := a.apiQuerier.init(nomadConfig); err != nil { return fmt.Errorf("error initializing API querier: %w", err) } return nil diff --git a/internal/nomad/nomad_test.go b/internal/nomad/nomad_test.go index d96eead..f6bdfb7 100644 --- a/internal/nomad/nomad_test.go +++ b/internal/nomad/nomad_test.go @@ -131,28 +131,38 @@ var ( const TestNamespace = "unit-tests" const TestNomadToken = "n0m4d-t0k3n" +const TestDefaultAddress = "127.0.0.1" + +func NomadTestConfig(address string) *config.Nomad { + return &config.Nomad{ + Address: address, + Port: 4646, + Token: TestNomadToken, + TLS: config.TLS{ + Active: false, + }, + Namespace: TestNamespace, + } +} func TestApiClient_init(t *testing.T) { client := &APIClient{apiQuerier: &nomadAPIClient{}} - err := client.init(&TestURL, TestNamespace, TestNomadToken) + err := client.init(NomadTestConfig(TestDefaultAddress)) require.Nil(t, err) } func TestApiClientCanNotBeInitializedWithInvalidUrl(t *testing.T) { client := &APIClient{apiQuerier: &nomadAPIClient{}} - err := client.init(&url.URL{ - Scheme: "http", - Host: "http://127.0.0.1:4646", - }, TestNamespace, TestNomadToken) + err := client.init(NomadTestConfig("http://" + TestDefaultAddress)) assert.NotNil(t, err) } func TestNewExecutorApiCanBeCreatedWithoutError(t *testing.T) { expectedClient := &APIClient{apiQuerier: &nomadAPIClient{}} - err := expectedClient.init(&TestURL, TestNamespace, TestNomadToken) + err := expectedClient.init(NomadTestConfig(TestDefaultAddress)) require.Nil(t, err) - _, err = NewExecutorAPI(&TestURL, TestNamespace, TestNomadToken) + _, err = NewExecutorAPI(NomadTestConfig(TestDefaultAddress)) require.Nil(t, err) } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index fc1d4ab..b3b96a2 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -51,7 +51,7 @@ func TestMain(m *testing.M) { } nomadNamespace = config.Config.Nomad.Namespace nomadClient, err = nomadApi.NewClient(&nomadApi.Config{ - Address: config.Config.NomadAPIURL().String(), + Address: config.Config.Nomad.URL().String(), TLSConfig: &nomadApi.TLSConfig{}, Namespace: nomadNamespace, }) diff --git a/tests/helpers/test_helpers.go b/tests/helpers/test_helpers.go index 3f3e193..db10356 100644 --- a/tests/helpers/test_helpers.go +++ b/tests/helpers/test_helpers.go @@ -24,7 +24,7 @@ import ( // BuildURL joins multiple route paths. func BuildURL(parts ...string) string { - url := config.Config.PoseidonAPIURL().String() + url := config.Config.Server.URL().String() for _, part := range parts { if !strings.HasPrefix(part, "/") { url += "/"