diff --git a/NOTICE b/NOTICE index 91fcfde29..2b6a4983e 100644 --- a/NOTICE +++ b/NOTICE @@ -653,9 +653,9 @@ License URL: https://github.com/ianlancetaylor/demangle/blob/96ee0021ea0f/LICENS ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.7.9 +Version: v6.7.10 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.9/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.10/LICENSE ---------- Module: github.com/jmoiron/sqlx @@ -707,9 +707,9 @@ License URL: https://github.com/lann/ps/blob/62de8c46ede0/LICENSE ---------- Module: github.com/lib/pq -Version: v1.12.0 +Version: v1.12.3 License: MIT -License URL: https://github.com/lib/pq/blob/v1.12.0/LICENSE +License URL: https://github.com/lib/pq/blob/v1.12.3/LICENSE ---------- Module: github.com/libopenstorage/secrets @@ -743,9 +743,9 @@ License URL: https://github.com/mattn/go-isatty/blob/v0.0.20/LICENSE ---------- Module: github.com/mattn/go-runewidth -Version: v0.0.21 +Version: v0.0.23 License: MIT -License URL: https://github.com/mattn/go-runewidth/blob/v0.0.21/LICENSE +License URL: https://github.com/mattn/go-runewidth/blob/v0.0.23/LICENSE ---------- Module: github.com/mitchellh/copystructure @@ -905,9 +905,9 @@ License URL: https://github.com/prometheus/procfs/blob/v0.20.1/LICENSE ---------- Module: github.com/rook/rook/pkg/apis/ceph.rook.io -Version: v0.0.0-20260417181530-5bd05dcbd0db +Version: v0.0.0-20260424083917-b87c434ff9a9 License: Apache-2.0 -License URL: https://github.com/rook/rook/blob/5bd05dcbd0db/pkg/apis/LICENSE +License URL: https://github.com/rook/rook/blob/b87c434ff9a9/pkg/apis/LICENSE ---------- Module: github.com/rubenv/sql-migrate @@ -1211,9 +1211,9 @@ License URL: https://github.com/grpc/grpc-go/blob/v1.80.0/LICENSE ---------- Module: google.golang.org/protobuf -Version: v1.36.11 +Version: v1.36.12-0.20260120151049-f2248ac996af License: BSD-3-Clause -License URL: https://github.com/protocolbuffers/protobuf-go/blob/v1.36.11/LICENSE +License URL: https://github.com/protocolbuffers/protobuf-go/blob/f2248ac996af/LICENSE ---------- Module: gopkg.in/evanphx/json-patch.v4 diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index 2f0ae4c27..665979f82 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -85,7 +85,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.InstallHash, "install-hash", "", "Codesphere package hash to install (default: none)") flags.StringArrayVarP(&bootstrapGcpCmd.CodesphereEnv.InstallSkipSteps, "install-skip-steps", "s", []string{}, "Installation steps to skip during Codesphere installation (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.RegistryUser, "registry-user", "", "Custom Registry username (only for GitHub registry type) (optional)") - flags.StringVar(&bootstrapGcpCmd.InputRegistryType, "registry-type", "local-container", "Container registry type to use (options: local-container, artifact-registry) (default: artifact-registry)") + flags.StringVar(&bootstrapGcpCmd.InputRegistryType, "registry-type", "local-container", "Container registry type to use (options: local-container, artifact-registry) (default: local-container)") flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.Experiments, "experiments", gcp.DefaultExperiments, "Experiments to enable in Codesphere installation (optional)") flags.StringArrayVar(&bootstrapGcpCmd.FeatureFlagList, "feature-flags", []string{}, "Feature flags to enable in Codesphere installation (optional)") @@ -94,6 +94,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.WriteConfig, "write-config", true, "Write generated install config to file (default: true)") flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.RecoverConfig, "recover-config", false, "Recover previously generated install config from the jumpbox. This will overwrite the local config! (default: false)") flags.BoolVar(&bootstrapGcpCmd.SSHQuiet, "ssh-quiet", false, "Suppress SSH command output (default: false)") + flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.CreateTestUser, "create-test-user", false, "Create a test user with API token on the bootstrapped instance for smoke testing (default: false)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OpenBaoURI, "openbao-uri", "", "URI for OpenBao (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OpenBaoEngine, "openbao-engine", "cs-secrets-engine", "OpenBao engine name (default: cs-secrets-engine)") @@ -137,6 +138,7 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { } c.CodesphereEnv.RegistryType = gcp.RegistryType(c.InputRegistryType) + c.CodesphereEnv.OmsWorkdir = c.Env.GetOmsWorkdir() if c.CodesphereEnv.GitHubPAT != "" { c.CodesphereEnv.RegistryType = gcp.RegistryTypeGitHub if c.CodesphereEnv.RegistryUser == "" { @@ -163,7 +165,8 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { } log.Println("\nšŸŽ‰šŸŽ‰šŸŽ‰ GCP infrastructure bootstrapped successfully!") - log.Printf("Access the jumpbox using:\nssh-add $SSH_KEY_PATH; ssh -o StrictHostKeyChecking=no -o ForwardAgent=yes -o SendEnv=OMS_PORTAL_API_KEY root@%s", bs.Env.Jumpbox.GetExternalIP()) + log.Printf("Access the jumpbox using:\nssh-add $SSH_KEY_PATH; ssh -o StrictHostKeyChecking=no -o ForwardAgent=yes -o SendEnv=OMS_PORTAL_API_KEY -o SendEnv=OMS_PORTAL_API root@%s", bs.Env.Jumpbox.GetExternalIP()) + if bs.Env.InstallVersion != "" { log.Printf("Access Codesphere in your web browser at https://cs.%s", bs.Env.BaseDomain) diff --git a/cli/cmd/create.go b/cli/cmd/create.go new file mode 100644 index 000000000..53f45a162 --- /dev/null +++ b/cli/cmd/create.go @@ -0,0 +1,27 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/spf13/cobra" +) + +// CreateCmd represents the create command group +type CreateCmd struct { + cmd *cobra.Command +} + +func AddCreateCmd(rootCmd *cobra.Command, opts *GlobalOptions) { + create := CreateCmd{ + cmd: &cobra.Command{ + Use: "create", + Short: "Create resources for Codesphere", + Long: io.Long(`Create resources for Codesphere installations, such as test users for automated testing.`), + }, + } + AddCmd(rootCmd, create.cmd) + + AddCreateTestUserCmd(create.cmd, opts) +} diff --git a/cli/cmd/create_test_user.go b/cli/cmd/create_test_user.go new file mode 100644 index 000000000..594ac2323 --- /dev/null +++ b/cli/cmd/create_test_user.go @@ -0,0 +1,69 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/env" + "github.com/codesphere-cloud/oms/internal/testuser" + "github.com/codesphere-cloud/oms/internal/util" +) + +type CreateTestUserCmd struct { + cmd *cobra.Command + Opts CreateTestUserOpts + Env env.Env +} + +type CreateTestUserOpts struct { + *GlobalOptions + testuser.CreateTestUserOpts +} + +func (c *CreateTestUserCmd) RunE(_ *cobra.Command, args []string) error { + result, err := testuser.CreateTestUser(c.Opts.CreateTestUserOpts) + if err != nil { + return fmt.Errorf("failed to create test user: %w", err) + } + + testuser.LogAndPersistResult(result, c.Env.GetOmsWorkdir()) + + return nil +} + +func AddCreateTestUserCmd(parent *cobra.Command, opts *GlobalOptions) { + c := CreateTestUserCmd{ + cmd: &cobra.Command{ + Use: "test-user", + Short: "Create a test user on a Codesphere database", + Long: io.Long(`Creates a test user with a hashed password and API token directly in a Codesphere + PostgreSQL database. The user can be used for automated smoke tests. + + The command connects to the specified PostgreSQL instance and creates the necessary + database records (credentials, email confirmation, team, team membership, API token). + + Credentials are displayed and saved to the OMS workdir as test-user.json.`), + }, + Opts: CreateTestUserOpts{GlobalOptions: opts}, + Env: env.NewEnv(), + } + c.cmd.RunE = c.RunE + + flags := c.cmd.Flags() + flags.StringVar(&c.Opts.Host, "postgres-host", "", "PostgreSQL host address (required)") + flags.IntVar(&c.Opts.Port, "postgres-port", testuser.DefaultPort, "PostgreSQL port") + flags.StringVar(&c.Opts.User, "postgres-user", testuser.DefaultUser, "PostgreSQL username") + flags.StringVar(&c.Opts.Password, "postgres-password", "", "PostgreSQL password (required)") + flags.StringVar(&c.Opts.DBName, "postgres-db", testuser.DefaultDBName, "PostgreSQL database name") + flags.StringVar(&c.Opts.SSLMode, "ssl-mode", testuser.DefaultSSLMode, "PostgreSQL SSL mode") + + util.MarkFlagRequired(c.cmd, "postgres-host") + util.MarkFlagRequired(c.cmd, "postgres-password") + + AddCmd(parent, c.cmd) +} diff --git a/cli/cmd/create_test_user_test.go b/cli/cmd/create_test_user_test.go new file mode 100644 index 000000000..cb74bdffc --- /dev/null +++ b/cli/cmd/create_test_user_test.go @@ -0,0 +1,96 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/codesphere-cloud/oms/cli/cmd" +) + +var _ = Describe("CreateTestUser", func() { + Context("AddCreateTestUserCmd", func() { + var createCmd cobra.Command + var opts *cmd.GlobalOptions + + BeforeEach(func() { + createCmd = cobra.Command{} + opts = &cmd.GlobalOptions{} + }) + + It("accepts valid flags with all required flags set", func() { + createCmd.SetArgs([]string{ + "test-user", + "--postgres-host", "localhost", + "--postgres-password", "secret", + }) + + cmd.AddCreateTestUserCmd(&createCmd, opts) + + createCmd.Commands()[0].RunE = func(cmd *cobra.Command, args []string) error { + return nil + } + + err := createCmd.Execute() + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails when --postgres-host is missing", func() { + createCmd.SetArgs([]string{ + "test-user", + "--postgres-password", "secret", + }) + + cmd.AddCreateTestUserCmd(&createCmd, opts) + + createCmd.Commands()[0].RunE = func(cmd *cobra.Command, args []string) error { + return nil + } + + err := createCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("postgres-host")) + }) + + It("fails when --postgres-password is missing", func() { + createCmd.SetArgs([]string{ + "test-user", + "--postgres-host", "localhost", + }) + + cmd.AddCreateTestUserCmd(&createCmd, opts) + + createCmd.Commands()[0].RunE = func(cmd *cobra.Command, args []string) error { + return nil + } + + err := createCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("postgres-password")) + }) + + It("accepts optional flags with custom values", func() { + createCmd.SetArgs([]string{ + "test-user", + "--postgres-host", "db.example.com", + "--postgres-password", "secret", + "--postgres-port", "5433", + "--postgres-user", "admin", + "--postgres-db", "mydb", + "--ssl-mode", "require", + }) + + cmd.AddCreateTestUserCmd(&createCmd, opts) + + createCmd.Commands()[0].RunE = func(cmd *cobra.Command, args []string) error { + return nil + } + + err := createCmd.Execute() + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2cc6dd53c..602a9773c 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -84,6 +84,9 @@ func GetRootCmd() *cobra.Command { // Smoke test commands AddSmoketestCmd(rootCmd, opts) + // Resource creation commands + AddCreateCmd(rootCmd, opts) + return rootCmd } diff --git a/docs/README.md b/docs/README.md index a47a666af..c3a10cccf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ like downloading new versions. * [oms beta](oms_beta.md) - Commands for early testing * [oms build](oms_build.md) - Build and push images to a registry +* [oms create](oms_create.md) - Create resources for Codesphere * [oms download](oms_download.md) - Download resources available through OMS * [oms init](oms_init.md) - Initialize configuration files * [oms install](oms_install.md) - Install Codesphere and other components diff --git a/docs/oms.md b/docs/oms.md index a47a666af..c3a10cccf 100644 --- a/docs/oms.md +++ b/docs/oms.md @@ -19,6 +19,7 @@ like downloading new versions. * [oms beta](oms_beta.md) - Commands for early testing * [oms build](oms_build.md) - Build and push images to a registry +* [oms create](oms_create.md) - Create resources for Codesphere * [oms download](oms_download.md) - Download resources available through OMS * [oms init](oms_init.md) - Initialize configuration files * [oms install](oms_install.md) - Install Codesphere and other components diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index 403f52994..93fca5d9d 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -19,6 +19,7 @@ oms beta bootstrap-gcp [flags] ``` --base-domain string Base domain for Codesphere (required) --billing-account string GCP Billing Account ID (required) + --create-test-user Create a test user with API token on the bootstrapped instance for smoke testing (default: false) --custom-pg-ip string Custom PostgreSQL IP (optional) --datacenter-id int Datacenter ID (default: 1) (default 1) --dns-project-id string GCP Project ID for Cloud DNS (optional) @@ -47,7 +48,7 @@ oms beta bootstrap-gcp [flags] --project-ttl string Time to live for the GCP project. Cleanup workflows will remove it afterwards. (default: 2 hours) (default "2h") --recover-config Recover previously generated install config from the jumpbox. This will overwrite the local config! (default: false) --region string GCP Region (default: europe-west4) (default "europe-west4") - --registry-type string Container registry type to use (options: local-container, artifact-registry) (default: artifact-registry) (default "local-container") + --registry-type string Container registry type to use (options: local-container, artifact-registry) (default: local-container) (default "local-container") --registry-user string Custom Registry username (only for GitHub registry type) (optional) --secrets-dir string Directory for secrets (default: /etc/codesphere/secrets) (default "/etc/codesphere/secrets") --secrets-file string Path to secrets files (optional) (default "prod.vault.yaml") diff --git a/docs/oms_create.md b/docs/oms_create.md new file mode 100644 index 000000000..424aceb40 --- /dev/null +++ b/docs/oms_create.md @@ -0,0 +1,19 @@ +## oms create + +Create resources for Codesphere + +### Synopsis + +Create resources for Codesphere installations, such as test users for automated testing. + +### Options + +``` + -h, --help help for create +``` + +### SEE ALSO + +* [oms](oms.md) - Codesphere Operations Management System (OMS) +* [oms create test-user](oms_create_test-user.md) - Create a test user on a Codesphere database + diff --git a/docs/oms_create_test-user.md b/docs/oms_create_test-user.md new file mode 100644 index 000000000..0df82c240 --- /dev/null +++ b/docs/oms_create_test-user.md @@ -0,0 +1,34 @@ +## oms create test-user + +Create a test user on a Codesphere database + +### Synopsis + +Creates a test user with a hashed password and API token directly in a Codesphere +PostgreSQL database. The user can be used for automated smoke tests. + +The command connects to the specified PostgreSQL instance and creates the necessary +database records (credentials, email confirmation, team, team membership, API token). + +Credentials are displayed and saved to the OMS workdir as test-user.json. + +``` +oms create test-user [flags] +``` + +### Options + +``` + -h, --help help for test-user + --postgres-db string PostgreSQL database name (default "codesphere") + --postgres-host string PostgreSQL host address (required) + --postgres-password string PostgreSQL password (required) + --postgres-port int PostgreSQL port (default 5432) + --postgres-user string PostgreSQL username (default "postgres") + --ssl-mode string PostgreSQL SSL mode (default "disable") +``` + +### SEE ALSO + +* [oms create](oms_create.md) - Create resources for Codesphere + diff --git a/go.mod b/go.mod index d3053fdd0..aed459ae9 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,8 @@ require ( ) require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/lib/pq v1.12.3 github.com/google/go-github/v74 v74.0.0 github.com/rook/rook/pkg/apis v0.0.0-20260424083917-b87c434ff9a9 ) @@ -585,7 +587,6 @@ require ( github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.12.0 // indirect github.com/libopenstorage/secrets v0.0.0-20240416031220-a17cf7f72c6c // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mattn/go-runewidth v0.0.23 // indirect diff --git a/go.sum b/go.sum index 337e98697..2014cc1b2 100644 --- a/go.sum +++ b/go.sum @@ -4177,6 +4177,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= @@ -4259,8 +4260,8 @@ github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFB github.com/letsencrypt/boulder v0.20260223.0 h1:xdS2OnJNUasR6TgVIOpqqcvdkOu47+PQQMBk9ThuWBw= github.com/letsencrypt/boulder v0.20260223.0/go.mod h1:r3aTSA7UZ7dbDfiGK+HLHJz0bWNbHk6YSPiXgzl23sA= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= -github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/libopenstorage/autopilot-api v0.6.1-0.20210128210103-5fbb67948648/go.mod h1:6JLrPbR3ZJQFbUY/+QJMl/aF00YdIrLf8/GWAplgvJs= github.com/libopenstorage/external-storage v5.2.0+incompatible/go.mod h1:H0Gzy0h36rbJPxu3lKdrIPw1h0k0c4YFtTGGlBEBPc8= github.com/libopenstorage/openstorage v8.0.0+incompatible/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 25ab91ac2..34884c511 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "log" "slices" "strings" "time" @@ -19,6 +20,7 @@ import ( "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/installer/node" "github.com/codesphere-cloud/oms/internal/portal" + "github.com/codesphere-cloud/oms/internal/testuser" "github.com/codesphere-cloud/oms/internal/util" "github.com/lithammer/shortuuid" "google.golang.org/api/dns/v1" @@ -140,6 +142,10 @@ type CodesphereEnvironment struct { Region string `json:"region"` Zone string `json:"zone"` DNSZoneName string `json:"dns_zone_name"` + + // Test user creation + CreateTestUser bool `json:"-"` + OmsWorkdir string `json:"-"` } func NewGCPBootstrapper( @@ -310,10 +316,49 @@ func (b *GCPBootstrapper) Bootstrap() error { } } + if b.Env.CreateTestUser { + if err := b.createTestUser(); err != nil { + log.Printf("warning: failed to create test user: %v", err) + } + } + return nil } -// ValidateInput checks that the required input parameters are set and valid +// createTestUser creates a test user in the PostgreSQL instance using the testuser package and logs the credentials. +func (b *GCPBootstrapper) createTestUser() error { + if b.Env.PostgreSQLNode == nil { + return fmt.Errorf("postgres node not found in bootstrap environment") + } + + pgHost := b.Env.PostgreSQLNode.GetExternalIP() + if pgHost == "" { + return fmt.Errorf("postgres node has no external IP") + } + + if b.Env.InstallConfig == nil { + return fmt.Errorf("install config not found in bootstrap environment") + } + pgPassword := b.Env.InstallConfig.Postgres.AdminPassword + if pgPassword == "" { + return fmt.Errorf("postgres admin password not found in install config") + } + + result, err := testuser.CreateTestUser(testuser.CreateTestUserOpts{ + Host: pgHost, + Port: testuser.DefaultPort, + User: testuser.DefaultUser, + Password: pgPassword, + DBName: testuser.DefaultDBName, + SSLMode: "require", + }) + if err != nil { + return err + } + + testuser.LogAndPersistResult(result, b.Env.OmsWorkdir) + return nil +} func (b *GCPBootstrapper) ValidateInput() error { err := b.validateInstallVersion() if err != nil { @@ -828,7 +873,8 @@ func (b *GCPBootstrapper) ensureCodespherePackageOnJumpbox() (string, error) { return "", fmt.Errorf("install hash must be set when install version is set") } b.stlog.Logf("Downloading Codesphere package...") - downloadCmd := fmt.Sprintf("oms download package -f %s -H %s %s", packageFilename, b.Env.InstallHash, b.Env.InstallVersion) + downloadCmd := fmt.Sprintf("oms download package -f %s -H %s %s", + packageFilename, b.Env.InstallHash, b.Env.InstallVersion) err := b.Env.Jumpbox.RunSSHCommand("root", downloadCmd) if err != nil { return "", fmt.Errorf("failed to download Codesphere package from jumpbox: %w", err) diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 9d2a2460d..931849fca 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -4,6 +4,7 @@ package node import ( + "bytes" "fmt" "log" "net" @@ -89,8 +90,11 @@ func (r *SSHNodeClient) RunCommand(n *Node, username string, command string) err defer util.IgnoreError(session.Close) _ = session.Setenv("OMS_PORTAL_API_KEY", os.Getenv("OMS_PORTAL_API_KEY")) + _ = session.Setenv("OMS_PORTAL_API", os.Getenv("OMS_PORTAL_API")) _ = agent.RequestAgentForwarding(session) // Best effort, ignore errors + var stderrBuf bytes.Buffer + session.Stderr = &stderrBuf if !r.Quiet { session.Stdout = os.Stdout session.Stderr = os.Stderr @@ -102,6 +106,9 @@ func (r *SSHNodeClient) RunCommand(n *Node, username string, command string) err if err := session.Wait(); err != nil { // A non-zero exit status from the remote command is also considered an error + if r.Quiet && stderrBuf.Len() > 0 { + return fmt.Errorf("command failed: %w\n%s", err, stderrBuf.String()) + } return fmt.Errorf("command failed: %w", err) } return nil @@ -209,7 +216,7 @@ func (n *Node) InstallOms() error { // HasAcceptEnvConfigured checks if AcceptEnv is configured func (n *Node) HasAcceptEnvConfigured() bool { - checkCommand := "sudo grep -E '^AcceptEnv OMS_PORTAL_API_KEY' /etc/ssh/sshd_config >/dev/null 2>&1" + checkCommand := "sudo grep -qxF 'AcceptEnv OMS_PORTAL_API_KEY OMS_PORTAL_API' /etc/ssh/sshd_config >/dev/null 2>&1" err := n.RunSSHCommand("ubuntu", checkCommand) if err != nil { // If the command returns a NON-zero exit status, it means AcceptEnv is not configured @@ -218,10 +225,10 @@ func (n *Node) HasAcceptEnvConfigured() bool { return true } -// ConfigureAcceptEnv configures AcceptEnv for OMS_PORTAL_API_KEY +// ConfigureAcceptEnv configures AcceptEnv for OMS_PORTAL_API_KEY and OMS_PORTAL_API func (n *Node) ConfigureAcceptEnv() error { cmds := []string{ - "sudo sed -i 's/^#\\?AcceptEnv.*/AcceptEnv OMS_PORTAL_API_KEY/' /etc/ssh/sshd_config", + "sudo sh -c \"grep -qxF 'AcceptEnv OMS_PORTAL_API_KEY OMS_PORTAL_API' /etc/ssh/sshd_config || printf '\\nAcceptEnv OMS_PORTAL_API_KEY OMS_PORTAL_API\\n' >> /etc/ssh/sshd_config\"", "sudo systemctl restart sshd", } for _, cmd := range cmds { diff --git a/internal/testuser/hash.go b/internal/testuser/hash.go new file mode 100644 index 000000000..4be3b1fa6 --- /dev/null +++ b/internal/testuser/hash.go @@ -0,0 +1,20 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package testuser + +import ( + "crypto/sha256" + "encoding/hex" +) + +// HashAPIToken hashes an API token using a single SHA256 with no additional salt. +func HashAPIToken(apiToken string) string { + return hashSecret(apiToken) +} + +func hashSecret(secret string) string { + hasher := sha256.New() + _, _ = hasher.Write([]byte(secret)) + return hex.EncodeToString(hasher.Sum(nil)) +} diff --git a/internal/testuser/hash_test.go b/internal/testuser/hash_test.go new file mode 100644 index 000000000..2c972a383 --- /dev/null +++ b/internal/testuser/hash_test.go @@ -0,0 +1,35 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package testuser + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HashAPIToken", func() { + It("produces a deterministic result", func() { + hash1 := HashAPIToken("testtoken") + hash2 := HashAPIToken("testtoken") + Expect(hash1).To(Equal(hash2)) + }) + + It("produces a valid 64-char hex string", func() { + hash := HashAPIToken("testtoken") + Expect(hash).To(HaveLen(64)) + Expect(hash).To(MatchRegexp("^[0-9a-f]{64}$")) + }) + + It("matches the expected known test vector", func() { + // SHA256("testtoken") + hash := HashAPIToken("testtoken") + Expect(hash).To(Equal("ada63e98fe50eccb55036d88eda4b2c3709f53c2b65bc0335797067e9a2a5d8b")) + }) + + It("produces different hashes for different inputs", func() { + hash1 := HashAPIToken("token1") + hash2 := HashAPIToken("token2") + Expect(hash1).NotTo(Equal(hash2)) + }) +}) diff --git a/internal/testuser/testuser.go b/internal/testuser/testuser.go new file mode 100644 index 000000000..4cc70997c --- /dev/null +++ b/internal/testuser/testuser.go @@ -0,0 +1,310 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package testuser + +import ( + "crypto/rand" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "time" + + // PostgreSQL driver + _ "github.com/lib/pq" +) + +const ( + TestEmail = "test@codesphere.com" + TestTeamName = "Tests" + tokenPrefix = "CS_" + + // Default connection parameters. + DefaultPort = 5432 + DefaultUser = "postgres" + DefaultDBName = "codesphere" + DefaultSSLMode = "disable" +) + +// CreateTestUserOpts contains the options for creating a test user. +type CreateTestUserOpts struct { + Host string + Port int + User string + Password string + DBName string + SSLMode string +} + +// TestUserResult contains the result of creating a test user. +type TestUserResult struct { + Email string `json:"email"` + PlaintextPassword string `json:"password"` + PlaintextAPIToken string `json:"api_token"` +} + +// TestUserCreator manages the lifecycle of test user creation. +type TestUserCreator struct { + opts CreateTestUserOpts + db *sql.DB +} + +// New creates a new TestUserCreator with the given options. +func New(opts CreateTestUserOpts) (*TestUserCreator, error) { + if opts.Port == 0 { + opts.Port = DefaultPort + } + if opts.User == "" { + opts.User = DefaultUser + } + if opts.DBName == "" { + opts.DBName = DefaultDBName + } + if opts.SSLMode == "" { + opts.SSLMode = DefaultSSLMode + } + if opts.Host == "" { + return nil, fmt.Errorf("host is required") + } + if opts.Password == "" { + return nil, fmt.Errorf("password is required") + } + + connStr := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s connect_timeout=10", + opts.Host, opts.Port, opts.User, opts.Password, opts.DBName, opts.SSLMode, + ) + + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open database connection: %w", err) + } + + db.SetConnMaxLifetime(30 * time.Second) + db.SetMaxOpenConns(1) + + if err := db.Ping(); err != nil { + _ = db.Close() + return nil, fmt.Errorf("failed to connect to database at %s:%d: %w", opts.Host, opts.Port, err) + } + + log.Printf("Connected to PostgreSQL at %s:%d", opts.Host, opts.Port) + return &TestUserCreator{opts: opts, db: db}, nil +} + +// close closes the underlying database connection. +func (c *TestUserCreator) close() error { + if c.db != nil { + return c.db.Close() + } + return nil +} + +// Create generates credentials and inserts the test user into the database. +func (c *TestUserCreator) Create() (*TestUserResult, error) { + plaintextPassword := os.Getenv("OMS_CS_TEST_USER_PASSWORD") + if plaintextPassword == "" { + return nil, fmt.Errorf("OMS_CS_TEST_USER_PASSWORD environment variable is not set") + } + hashedPassword := os.Getenv("OMS_CS_TEST_USER_PASSWORD_HASHED") + if hashedPassword == "" { + return nil, fmt.Errorf("OMS_CS_TEST_USER_PASSWORD_HASHED environment variable is not set") + } + + plaintextToken, err := generateAPIToken() + if err != nil { + return nil, fmt.Errorf("failed to generate API token: %w", err) + } + + hashedToken := HashAPIToken(plaintextToken) + + result, err := c.createInDB(hashedPassword, hashedToken) + if err != nil { + return nil, err + } + + result.PlaintextPassword = plaintextPassword + result.PlaintextAPIToken = plaintextToken + + return result, nil +} + +// CreateTestUser is a convenience facade: New -> Create -> close. +func CreateTestUser(opts CreateTestUserOpts) (*TestUserResult, error) { + creator, err := New(opts) + if err != nil { + return nil, err + } + defer func() { _ = creator.close() }() + return creator.Create() +} + +// createInDB executes the database inserts inside a transaction. +func (c *TestUserCreator) createInDB(hashedPassword, hashedToken string) (*TestUserResult, error) { + exists, err := c.userExists() + if err != nil { + return nil, err + } + if exists { + return nil, fmt.Errorf("test user %s already exists", TestEmail) + } + + tx, err := c.db.Begin() + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer func() { _ = tx.Rollback() }() + + userID, err := c.insertCredentials(tx, hashedPassword) + if err != nil { + return nil, err + } + + if err := c.insertEmailConfirmation(tx); err != nil { + return nil, err + } + + teamID, err := c.insertTeam(tx) + if err != nil { + return nil, err + } + + if err := c.insertTeamMember(tx, userID, teamID); err != nil { + return nil, err + } + + if err := c.insertAPIToken(tx, hashedToken, userID); err != nil { + return nil, err + } + + if err = tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + log.Printf("Test user created: email=%s, userID=%d, teamID=%d", TestEmail, userID, teamID) + + return &TestUserResult{Email: TestEmail}, nil +} + +func (c *TestUserCreator) userExists() (bool, error) { + var exists bool + err := c.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM authservice.credentials WHERE email = $1)`, TestEmail).Scan(&exists) + if err != nil { + return false, fmt.Errorf("failed to check for existing test user: %w", err) + } + return exists, nil +} + +func (c *TestUserCreator) insertCredentials(tx *sql.Tx, hashedPassword string) (int, error) { + var userID int + err := tx.QueryRow(` + INSERT INTO authservice.credentials + (user_id, email, password_hash, authentication_method, signed_up, banned) + VALUES(nextval('authservice.credentials_user_id_seq'::regclass), $1, $2, 'password'::text, false, false) + RETURNING user_id`, + TestEmail, hashedPassword, + ).Scan(&userID) + if err != nil { + return 0, fmt.Errorf("failed to insert credentials: %w", err) + } + return userID, nil +} + +func (c *TestUserCreator) insertEmailConfirmation(tx *sql.Tx) error { + _, err := tx.Exec(` + INSERT INTO authservice.email_confirmations + (id, email, pending, created_at) + VALUES(uuid_generate_v4(), $1, false, CURRENT_TIMESTAMP)`, + TestEmail, + ) + if err != nil { + return fmt.Errorf("failed to insert email confirmation: %w", err) + } + return nil +} + +func (c *TestUserCreator) insertTeam(tx *sql.Tx) (int, error) { + var teamID int + err := tx.QueryRow(` + INSERT INTO "teamService".teams + (id, "name", description, first_team, default_data_center_id, deleted, deletion_pending, created_at) + VALUES(nextval('"teamService".teams_id_seq'::regclass), $1, '', true, 1, false, false, CURRENT_TIMESTAMP) + RETURNING id`, + TestTeamName, + ).Scan(&teamID) + if err != nil { + return 0, fmt.Errorf("failed to insert team: %w", err) + } + return teamID, nil +} + +func (c *TestUserCreator) insertTeamMember(tx *sql.Tx, userID, teamID int) error { + _, err := tx.Exec(` + INSERT INTO "teamService".team_members + (user_id, team_id, "role", pending, created_at) + VALUES($1, $2, 0, false, CURRENT_TIMESTAMP)`, + userID, teamID, + ) + if err != nil { + return fmt.Errorf("failed to insert team member: %w", err) + } + return nil +} + +func (c *TestUserCreator) insertAPIToken(tx *sql.Tx, hashedToken string, userID int) error { + _, err := tx.Exec(` + INSERT INTO public_api_service.tokens + (id, "token", user_id, "name", created_at) + VALUES(nextval('public_api_service.tokens_id_seq'::regclass), $1, $2, 'testkey', now())`, + hashedToken, userID, + ) + if err != nil { + return fmt.Errorf("failed to insert API token: %w", err) + } + return nil +} + +// LogAndPersistResult writes the test user result to a JSON file and logs the credentials. +func LogAndPersistResult(result *TestUserResult, workdir string) { + filePath, err := WriteResultToFile(result, workdir) + if err != nil { + log.Printf("warning: failed to write test user result to file: %v", err) + } else { + log.Printf("Test user credentials written to %s", filePath) + } + + log.Printf("Email: %s", result.Email) + log.Printf("Password: %s", result.PlaintextPassword) + log.Printf("API Token: %s", result.PlaintextAPIToken) +} + +// WriteResultToFile writes the test user result to a JSON file in the given directory. +func WriteResultToFile(result *TestUserResult, dir string) (string, error) { + data, err := json.MarshalIndent(result, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal test user result: %w", err) + } + + if err := os.MkdirAll(dir, 0700); err != nil { + return "", fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + filePath := filepath.Join(dir, "test-user.json") + if err := os.WriteFile(filePath, data, 0600); err != nil { + return "", fmt.Errorf("failed to write test user file: %w", err) + } + + return filePath, nil +} + +func generateAPIToken() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", err + } + return tokenPrefix + hex.EncodeToString(b), nil +} diff --git a/internal/testuser/testuser_suite_test.go b/internal/testuser/testuser_suite_test.go new file mode 100644 index 000000000..2770b685d --- /dev/null +++ b/internal/testuser/testuser_suite_test.go @@ -0,0 +1,16 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package testuser + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTestuser(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testuser Suite") +} diff --git a/internal/testuser/testuser_test.go b/internal/testuser/testuser_test.go new file mode 100644 index 000000000..b83e5f542 --- /dev/null +++ b/internal/testuser/testuser_test.go @@ -0,0 +1,220 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package testuser + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/DATA-DOG/go-sqlmock" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("generateAPIToken", func() { + It("returns no error", func() { + _, err := generateAPIToken() + Expect(err).NotTo(HaveOccurred()) + }) + + It("starts with the CS_ prefix", func() { + token, err := generateAPIToken() + Expect(err).NotTo(HaveOccurred()) + Expect(token).To(HavePrefix("CS_")) + }) + + It("has the correct length (3 prefix + 32 hex chars)", func() { + token, err := generateAPIToken() + Expect(err).NotTo(HaveOccurred()) + Expect(token).To(HaveLen(35)) + }) + + It("produces unique tokens on successive calls", func() { + token1, err := generateAPIToken() + Expect(err).NotTo(HaveOccurred()) + token2, err := generateAPIToken() + Expect(err).NotTo(HaveOccurred()) + Expect(token1).NotTo(Equal(token2)) + }) + + It("contains only hex characters after the prefix", func() { + token, err := generateAPIToken() + Expect(err).NotTo(HaveOccurred()) + hexPart := token[len(tokenPrefix):] + Expect(hexPart).To(MatchRegexp("^[0-9a-f]{32}$")) + }) +}) + +var _ = Describe("WriteResultToFile", func() { + It("writes a valid JSON file to the given directory", func() { + dir := GinkgoT().TempDir() + result := &TestUserResult{ + Email: "test@example.com", + PlaintextPassword: "secret123", + PlaintextAPIToken: "CS_abc123", + } + + filePath, err := WriteResultToFile(result, dir) + Expect(err).NotTo(HaveOccurred()) + Expect(filePath).To(Equal(filepath.Join(dir, "test-user.json"))) + + data, err := os.ReadFile(filePath) + Expect(err).NotTo(HaveOccurred()) + + var loaded TestUserResult + err = json.Unmarshal(data, &loaded) + Expect(err).NotTo(HaveOccurred()) + Expect(loaded.Email).To(Equal("test@example.com")) + Expect(loaded.PlaintextPassword).To(Equal("secret123")) + Expect(loaded.PlaintextAPIToken).To(Equal("CS_abc123")) + }) + + It("creates the directory if it does not exist", func() { + dir := filepath.Join(GinkgoT().TempDir(), "nested", "subdir") + result := &TestUserResult{ + Email: "user@example.com", + PlaintextPassword: "pass", + PlaintextAPIToken: "CS_token", + } + + filePath, err := WriteResultToFile(result, dir) + Expect(err).NotTo(HaveOccurred()) + Expect(filePath).To(BeARegularFile()) + }) + + It("sets restrictive file permissions (0600)", func() { + dir := GinkgoT().TempDir() + result := &TestUserResult{ + Email: "user@example.com", + PlaintextPassword: "pass", + PlaintextAPIToken: "CS_token", + } + + filePath, err := WriteResultToFile(result, dir) + Expect(err).NotTo(HaveOccurred()) + + info, err := os.Stat(filePath) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode().Perm()).To(Equal(os.FileMode(0600))) + }) +}) + +var _ = Describe("createInDB", func() { + const ( + hashedPassword = "fakehashedpassword" + hashedToken = "fakehashedtoken" + ) + + It("creates a test user successfully", func() { + sqlDB, m, err := sqlmock.New() + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = sqlDB.Close() }() + + // Expect: check if user exists + m.ExpectQuery(`SELECT EXISTS`). + WithArgs(TestEmail). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false)) + + // Expect: begin transaction + m.ExpectBegin() + + // Expect: insert credentials + m.ExpectQuery(`INSERT INTO authservice.credentials`). + WithArgs(TestEmail, hashedPassword). + WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(42)) + + // Expect: insert email confirmation + m.ExpectExec(`INSERT INTO authservice.email_confirmations`). + WithArgs(TestEmail). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // Expect: insert team + m.ExpectQuery(`INSERT INTO "teamService".teams`). + WithArgs(TestTeamName). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(7)) + + // Expect: insert team member + m.ExpectExec(`INSERT INTO "teamService".team_members`). + WithArgs(42, 7). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // Expect: insert API token + m.ExpectExec(`INSERT INTO public_api_service.tokens`). + WithArgs(hashedToken, 42). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // Expect: commit + m.ExpectCommit() + + result, err := (&TestUserCreator{opts: CreateTestUserOpts{Host: "test", Password: "test"}, db: sqlDB}).createInDB(hashedPassword, hashedToken) + Expect(err).NotTo(HaveOccurred()) + Expect(result.Email).To(Equal(TestEmail)) + Expect(m.ExpectationsWereMet()).NotTo(HaveOccurred()) + }) + + It("returns an error when the test user already exists", func() { + sqlDB, m, err := sqlmock.New() + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = sqlDB.Close() }() + + m.ExpectQuery(`SELECT EXISTS`). + WithArgs(TestEmail). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + + _, err = (&TestUserCreator{opts: CreateTestUserOpts{Host: "test", Password: "test"}, db: sqlDB}).createInDB(hashedPassword, hashedToken) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("already exists")) + Expect(m.ExpectationsWereMet()).NotTo(HaveOccurred()) + }) + + It("rolls back the transaction on credential insert failure", func() { + sqlDB, m, err := sqlmock.New() + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = sqlDB.Close() }() + + m.ExpectQuery(`SELECT EXISTS`). + WithArgs(TestEmail). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false)) + + m.ExpectBegin() + m.ExpectQuery(`INSERT INTO authservice.credentials`). + WithArgs(TestEmail, hashedPassword). + WillReturnError(fmt.Errorf("unique_violation")) + m.ExpectRollback() + + _, err = (&TestUserCreator{opts: CreateTestUserOpts{Host: "test", Password: "test"}, db: sqlDB}).createInDB(hashedPassword, hashedToken) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to insert credentials")) + Expect(m.ExpectationsWereMet()).NotTo(HaveOccurred()) + }) + + It("rolls back the transaction on team insert failure", func() { + sqlDB, m, err := sqlmock.New() + Expect(err).NotTo(HaveOccurred()) + defer func() { _ = sqlDB.Close() }() + + m.ExpectQuery(`SELECT EXISTS`). + WithArgs(TestEmail). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false)) + + m.ExpectBegin() + m.ExpectQuery(`INSERT INTO authservice.credentials`). + WithArgs(TestEmail, hashedPassword). + WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(42)) + m.ExpectExec(`INSERT INTO authservice.email_confirmations`). + WithArgs(TestEmail). + WillReturnResult(sqlmock.NewResult(1, 1)) + m.ExpectQuery(`INSERT INTO "teamService".teams`). + WithArgs(TestTeamName). + WillReturnError(fmt.Errorf("db error")) + m.ExpectRollback() + + _, err = (&TestUserCreator{opts: CreateTestUserOpts{Host: "test", Password: "test"}, db: sqlDB}).createInDB(hashedPassword, hashedToken) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to insert team")) + Expect(m.ExpectationsWereMet()).NotTo(HaveOccurred()) + }) +}) diff --git a/internal/tmpl/NOTICE b/internal/tmpl/NOTICE index 91fcfde29..2b6a4983e 100644 --- a/internal/tmpl/NOTICE +++ b/internal/tmpl/NOTICE @@ -653,9 +653,9 @@ License URL: https://github.com/ianlancetaylor/demangle/blob/96ee0021ea0f/LICENS ---------- Module: github.com/jedib0t/go-pretty/v6 -Version: v6.7.9 +Version: v6.7.10 License: MIT -License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.9/LICENSE +License URL: https://github.com/jedib0t/go-pretty/blob/v6.7.10/LICENSE ---------- Module: github.com/jmoiron/sqlx @@ -707,9 +707,9 @@ License URL: https://github.com/lann/ps/blob/62de8c46ede0/LICENSE ---------- Module: github.com/lib/pq -Version: v1.12.0 +Version: v1.12.3 License: MIT -License URL: https://github.com/lib/pq/blob/v1.12.0/LICENSE +License URL: https://github.com/lib/pq/blob/v1.12.3/LICENSE ---------- Module: github.com/libopenstorage/secrets @@ -743,9 +743,9 @@ License URL: https://github.com/mattn/go-isatty/blob/v0.0.20/LICENSE ---------- Module: github.com/mattn/go-runewidth -Version: v0.0.21 +Version: v0.0.23 License: MIT -License URL: https://github.com/mattn/go-runewidth/blob/v0.0.21/LICENSE +License URL: https://github.com/mattn/go-runewidth/blob/v0.0.23/LICENSE ---------- Module: github.com/mitchellh/copystructure @@ -905,9 +905,9 @@ License URL: https://github.com/prometheus/procfs/blob/v0.20.1/LICENSE ---------- Module: github.com/rook/rook/pkg/apis/ceph.rook.io -Version: v0.0.0-20260417181530-5bd05dcbd0db +Version: v0.0.0-20260424083917-b87c434ff9a9 License: Apache-2.0 -License URL: https://github.com/rook/rook/blob/5bd05dcbd0db/pkg/apis/LICENSE +License URL: https://github.com/rook/rook/blob/b87c434ff9a9/pkg/apis/LICENSE ---------- Module: github.com/rubenv/sql-migrate @@ -1211,9 +1211,9 @@ License URL: https://github.com/grpc/grpc-go/blob/v1.80.0/LICENSE ---------- Module: google.golang.org/protobuf -Version: v1.36.11 +Version: v1.36.12-0.20260120151049-f2248ac996af License: BSD-3-Clause -License URL: https://github.com/protocolbuffers/protobuf-go/blob/v1.36.11/LICENSE +License URL: https://github.com/protocolbuffers/protobuf-go/blob/f2248ac996af/LICENSE ---------- Module: gopkg.in/evanphx/json-patch.v4