diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 578a71618..0dc2ea76d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,9 +12,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-tags: true - fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v4 @@ -27,9 +24,6 @@ jobs: - name: Install juju-db run: sudo snap install juju-db --channel 4.4/stable - - name: Create test certs - run: make certs - - name: Start test environment run: docker compose up -d --wait @@ -40,11 +34,3 @@ jobs: - name: Build and Test run: go test -mod readonly ./... -timeout 1h -cover - env: - JIMM_DSN: postgresql://jimm:jimm@localhost:5432/jimm - JIMM_TEST_PGXDSN: postgresql://jimm:jimm@localhost:5432/jimm - PGHOST: localhost - PGPASSWORD: jimm - PGSSLMODE: disable - PGUSER: jimm - PGPORT: 5432 diff --git a/Makefile b/Makefile index cb95caf0e..9682e87b1 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ clean: certs: @cd local/traefik/certs; ./certs.sh; cd - -test-env: sys-deps certs +test-env: sys-deps @docker compose up --force-recreate -d --wait test-env-cleanup: diff --git a/cmd/jimmctl/cmd/export_test.go b/cmd/jimmctl/cmd/export_test.go index eb2569252..cb90906d1 100644 --- a/cmd/jimmctl/cmd/export_test.go +++ b/cmd/jimmctl/cmd/export_test.go @@ -8,6 +8,8 @@ import ( "github.com/juju/juju/cloud" "github.com/juju/juju/cmd/modelcmd" "github.com/juju/juju/jujuclient" + + "github.com/canonical/jimm/v3/internal/cmdtest" ) var ( @@ -20,13 +22,18 @@ var ( type AccessResult = accessResult +func testDialOpts(lp jujuapi.LoginProvider) *jujuapi.DialOpts { + return &jujuapi.DialOpts{ + InsecureSkipVerify: true, + LoginProvider: lp, + DialWebsocket: cmdtest.GetDialWebsocketWithInsecureUrl(), + } +} + func NewListControllersCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listControllersCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -34,11 +41,8 @@ func NewListControllersCommandForTesting(store jujuclient.ClientStore, lp jujuap func NewModelStatusCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &modelStatusCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -46,11 +50,8 @@ func NewModelStatusCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewGrantAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &grantAuditLogAccessCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -58,11 +59,8 @@ func NewGrantAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp ju func NewRevokeAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &revokeAuditLogAccessCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -70,11 +68,8 @@ func NewRevokeAuditLogAccessCommandForTesting(store jujuclient.ClientStore, lp j func NewListAuditEventsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listAuditEventsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -84,10 +79,7 @@ func NewAddCloudToControllerCommandForTesting(store jujuclient.ClientStore, lp j cmd := &addCloudToControllerCommand{ store: store, cloudByNameFunc: cloudByNameFunc, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -97,11 +89,8 @@ type RemoveCloudFromControllerAPI = removeCloudFromControllerAPI func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider, removeCloudFromControllerAPIFunc func() (RemoveCloudFromControllerAPI, error)) cmd.Command { cmd := &removeCloudFromControllerCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), removeCloudFromControllerAPIFunc: removeCloudFromControllerAPIFunc, } if removeCloudFromControllerAPIFunc == nil { @@ -113,11 +102,8 @@ func NewRemoveCloudFromControllerCommandForTesting(store jujuclient.ClientStore, func NewAddControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addControllerCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -125,11 +111,8 @@ func NewAddControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi. func NewRemoveControllerCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeControllerCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -145,11 +128,8 @@ func NewControllerInfoCommandForTesting(store jujuclient.ClientStore) cmd.Comman func NewSetControllerDeprecatedCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &setControllerDeprecatedCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -157,11 +137,8 @@ func NewSetControllerDeprecatedCommandForTesting(store jujuclient.ClientStore, l func NewImportModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &importModelCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -169,11 +146,8 @@ func NewImportModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewUpdateMigratedModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &updateMigratedModelCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -181,11 +155,8 @@ func NewUpdateMigratedModelCommandForTesting(store jujuclient.ClientStore, lp ju func NewImportCloudCredentialsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &importCloudCredentialsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -193,11 +164,8 @@ func NewImportCloudCredentialsCommandForTesting(store jujuclient.ClientStore, lp func NewAddGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addGroupCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -205,11 +173,8 @@ func NewAddGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Login func NewRenameGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &renameGroupCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -217,11 +182,8 @@ func NewRenameGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewRemoveGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeGroupCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -229,11 +191,8 @@ func NewRemoveGroupCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewListGroupsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listGroupsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -241,11 +200,8 @@ func NewListGroupsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Log func NewAddRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &addRelationCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -253,11 +209,8 @@ func NewAddRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Lo func NewRemoveRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &removeRelationCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -265,11 +218,8 @@ func NewRemoveRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi func NewListRelationsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &listRelationsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -277,11 +227,8 @@ func NewListRelationsCommandForTesting(store jujuclient.ClientStore, lp jujuapi. func NewCheckRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &checkRelationCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -289,11 +236,8 @@ func NewCheckRelationCommandForTesting(store jujuclient.ClientStore, lp jujuapi. func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &crossModelQueryCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -301,11 +245,8 @@ func NewCrossModelQueryCommandForTesting(store jujuclient.ClientStore, lp jujuap func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &purgeLogsCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) @@ -313,11 +254,8 @@ func NewPurgeLogsCommandForTesting(store jujuclient.ClientStore, lp jujuapi.Logi func NewMigrateModelCommandForTesting(store jujuclient.ClientStore, lp jujuapi.LoginProvider) cmd.Command { cmd := &migrateModelCommand{ - store: store, - dialOpts: &jujuapi.DialOpts{ - InsecureSkipVerify: true, - LoginProvider: lp, - }, + store: store, + dialOpts: testDialOpts(lp), } return modelcmd.WrapBase(cmd) diff --git a/internal/cmdtest/jimmsuite.go b/internal/cmdtest/jimmsuite.go index b703a7fd8..a5e34c56e 100644 --- a/internal/cmdtest/jimmsuite.go +++ b/internal/cmdtest/jimmsuite.go @@ -5,23 +5,22 @@ package cmdtest import ( - "bytes" "context" "crypto/tls" - "encoding/pem" "net/http" "net/http/httptest" "net/url" "os" - "path/filepath" "strings" "time" cofga "github.com/canonical/ofga" + "github.com/gorilla/websocket" "github.com/juju/juju/api" "github.com/juju/juju/core/network" corejujutesting "github.com/juju/juju/juju/testing" jjclient "github.com/juju/juju/jujuclient" + "github.com/juju/juju/rpc/jsoncodec" jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v5" gc "gopkg.in/check.v1" @@ -58,8 +57,7 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.cancel = cancel s.HTTP = httptest.NewUnstartedServer(nil) - s.HTTP.TLS = setupTLS(c) - u, err := url.Parse("https://" + s.HTTP.Listener.Addr().String()) + u, err := url.Parse("http://" + s.HTTP.Listener.Addr().String()) c.Assert(err, gc.Equals, nil) ofgaClient, cofgaClient, cofgaParams, err := jimmtest.SetupTestOFGAClient(c.TestName()) @@ -95,9 +93,9 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { err = s.Service.StartJWKSRotator(ctx, time.NewTicker(time.Hour).C, time.Now().UTC().AddDate(0, 3, 0)) c.Assert(err, gc.Equals, nil) - s.HTTP.StartTLS() + s.HTTP.Start() - // NOW we can set up the juju conn suites + // Now we can set up the juju conn suites s.ControllerConfigAttrs = map[string]interface{}{ "login-token-refresh-url": u.String() + "/.well-known/jwks.json", } @@ -110,13 +108,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { s.AddAdminUser(c, "alice@canonical.com") - w := new(bytes.Buffer) - err = pem.Encode(w, &pem.Block{ - Type: "CERTIFICATE", - Bytes: s.HTTP.TLS.Certificates[0].Certificate[0], - }) - c.Assert(err, gc.Equals, nil) - s.ClientStore = func() *jjclient.MemStore { store := jjclient.NewMemStore() store.CurrentControllerName = "JIMM" @@ -124,7 +115,6 @@ func (s *JimmCmdSuite) SetUpTest(c *gc.C) { ControllerUUID: "914487b5-60e7-42bb-bd63-1adc3fd3a388", APIEndpoints: []string{u.Host}, PublicDNSName: s.HTTP.URL, - CACert: w.String(), } return store } @@ -153,41 +143,22 @@ func (s *JimmCmdSuite) TearDownTest(c *gc.C) { s.JujuConnSuite.TearDownTest(c) } -func getRootJimmPath(c *gc.C) string { - path, err := os.Getwd() - c.Assert(err, gc.IsNil) - dirs := strings.Split(path, "/") - c.Assert(len(dirs), gc.Not(gc.Equals), 1) - dirs = dirs[1:] - jimmIndex := -1 - // Range over dirs from the end to ensure no top-level jimm - // folders interfere with our search. - for i := len(dirs) - 1; i >= 0; i-- { - if dirs[i] == "jimm" { - jimmIndex = i + 1 - break +// GetDialWebsocketWithInsecureUrl forces the URL used for dialing to use insecure websockets +// so that tests don't need to start an HTTPS server and manage certs. +func GetDialWebsocketWithInsecureUrl() func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + // Modified from github.com/juju/juju@v0.0.0-20240304110523-55fb5d03683b/api/apiclient.go gorillaDialWebsocket + + dialWebsocket := func(ctx context.Context, urlStr string, tlsConfig *tls.Config, ipAddr string) (jsoncodec.JSONConn, error) { + urlStr = strings.Replace(urlStr, "wss", "ws", 1) + dialer := &websocket.Dialer{} + c, resp, err := dialer.Dial(urlStr, nil) + defer resp.Body.Close() + if err != nil { + return nil, err } + return jsoncodec.NewWebsocketConn(c), nil } - c.Assert(jimmIndex, gc.Not(gc.Equals), -1) - return "/" + filepath.Join(dirs[:jimmIndex]...) -} - -func setupTLS(c *gc.C) *tls.Config { - jimmPath := getRootJimmPath(c) - pathToCert := filepath.Join(jimmPath, "local/traefik/certs/server.crt") - localhostCert, err := os.ReadFile(pathToCert) - c.Assert(err, gc.IsNil, gc.Commentf("Unable to find cert at %s. Run make cert in root directory.", pathToCert)) - - pathToKey := filepath.Join(jimmPath, "local/traefik/certs/server.key") - localhostKey, err := os.ReadFile(pathToKey) - c.Assert(err, gc.IsNil, gc.Commentf("Unable to find key at %s. Run make cert in root directory.", pathToKey)) - - cert, err := tls.X509KeyPair(localhostCert, localhostKey) - c.Assert(err, gc.IsNil, gc.Commentf("Failed to generate certificate key pair.")) - - tlsConfig := new(tls.Config) - tlsConfig.Certificates = []tls.Certificate{cert} - return tlsConfig + return dialWebsocket } func (s *JimmCmdSuite) AddAdminUser(c *gc.C, email string) {