diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index 368746c7..5099b681 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -74,8 +74,6 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OidcIssuerURL, "oidc-issuer-url", "", "OIDC OAuth provider issuer URL (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OidcClientID, "oidc-client-id", "", "OIDC OAuth provider Client ID (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OidcClientSecret, "oidc-client-secret", "", "OIDC OAuth provider Client Secret (optional)") - flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.CentralOtelUsername, "central-otel-username", "", "Central OpenTelemetry username. Needed when sending spans to central collector (optional)") - flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.CentralOtelPassword, "central-otel-password", "", "Central OpenTelemetry password. Needed when sending spans to central collector (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubPAT, "github-pat", "", "GitHub Personal Access Token used for direct image access and fetching team SSH keys. Required when using --github-team-org/--github-team-slug. Required scopes: read:packages, read:org.") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubAppName, "github-app-name", "", "GitHub App Name (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.GitHubTeamOrg, "github-team-org", "", "GitHub organization used to fetch team SSH keys (optional, used with --github-team-slug). Requires --github-pat with at least the read:org scope.") @@ -118,6 +116,12 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OpenBaoUser, "openbao-user", "admin", "OpenBao username (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.OpenBaoPassword, "openbao-password", "", "OpenBao password (optional)") + flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.CentralOtelEndpoint, "central-otel-endpoint", "", "Central OpenTelemetry collector endpoint. Needed when sending spans to central collector (optional)") + flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.CentralOtelUsername, "central-otel-username", "", "Central OpenTelemetry username. Needed when sending spans to central collector (optional)") + flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.CentralOtelPassword, "central-otel-password", "", "Central OpenTelemetry password. Needed when sending spans to central collector (optional)") + flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.CentralOtelSpanMetrics, "central-otel-span-metrics", false, "Enable span metrics in Central OpenTelemetry export (default: false)") + flags.BoolVar(&bootstrapGcpCmd.CodesphereEnv.LocalTraceExport, "local-trace-export", false, "Enable local trace export (default: false)") + util.MarkFlagRequired(bootstrapGcpCmd.cmd, "project-name") util.MarkFlagRequired(bootstrapGcpCmd.cmd, "billing-account") util.MarkFlagRequired(bootstrapGcpCmd.cmd, "base-domain") diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index b3f168cb..67505415 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -23,7 +23,9 @@ oms beta bootstrap-gcp [flags] --billing-account string GCP Billing Account ID (required) --bitbucket-app-client-id string Bitbucket App Client ID (optional) --bitbucket-app-client-secret string Bitbucket App Client Secret (optional) + --central-otel-endpoint string Central OpenTelemetry collector endpoint. Needed when sending spans to central collector (optional) --central-otel-password string Central OpenTelemetry password. Needed when sending spans to central collector (optional) + --central-otel-span-metrics Enable span metrics in Central OpenTelemetry export (default: false) --central-otel-username string Central OpenTelemetry username. Needed when sending spans to central collector (optional) --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) @@ -51,6 +53,7 @@ oms beta bootstrap-gcp [flags] --install-local string Install Codesphere from local package (default: none) -s, --install-skip-steps stringArray Installation steps to skip during Codesphere installation (optional) --install-version string Codesphere version to install (default: none) + --local-trace-export Enable local trace export (default: false) --oidc-client-id string OIDC OAuth provider Client ID (optional) --oidc-client-secret string OIDC OAuth provider Client Secret (optional) --oidc-issuer-url string OIDC OAuth provider issuer URL (optional) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 0eff6500..35b3ae8e 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -114,8 +114,6 @@ type CodesphereEnvironment struct { RegistryUser string `json:"-"` Experiments []string `json:"experiments"` FeatureFlags map[string]bool `json:"feature_flags"` - CentralOtelUsername string `json:"-"` - CentralOtelPassword string `json:"-"` ExternalLokiEndpoint string `json:"external_loki_endpoint,omitempty"` ExternalLokiSecret string `json:"-"` ExternalLokiUser string `json:"external_loki_user,omitempty"` @@ -126,6 +124,12 @@ type CodesphereEnvironment struct { OpenBaoUser string `json:"-"` OpenBaoPassword string `json:"-"` + CentralOtelEndpoint string `json:"-"` + CentralOtelUsername string `json:"-"` + CentralOtelPassword string `json:"-"` + CentralOtelSpanMetrics bool `json:"-"` + LocalTraceExport bool `json:"-"` + // Config InstallConfigPath string `json:"-"` SecretsFilePath string `json:"-"` @@ -403,7 +407,12 @@ func (b *GCPBootstrapper) ValidateInput() error { return err } - return b.validateExternalLokiParams() + err = b.validateExternalLokiParams() + if err != nil { + return err + } + + return b.validateCentralOtelParams() } // validateInstallVersion checks if the specified install version exists and contains the required installer artifact @@ -510,6 +519,21 @@ func (b *GCPBootstrapper) validateExternalLokiParams() error { return nil } +func (b *GCPBootstrapper) validateCentralOtelParams() error { + if b.Env.CentralOtelEndpoint != "" && b.Env.CentralOtelPassword == "" { + return fmt.Errorf("central OTel password is required when central OTel endpoint is set") + } + + if b.Env.CentralOtelUsername != "" && b.Env.CentralOtelPassword == "" { + return fmt.Errorf("central OTel username is set but password is missing") + } + if b.Env.CentralOtelPassword != "" && b.Env.CentralOtelUsername == "" { + return fmt.Errorf("central OTel password is set but username is missing") + } + + return nil +} + func (b *GCPBootstrapper) EnsureArtifactRegistry() error { repoName := "codesphere-registry" diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index 6247faf3..79af7f2a 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -574,6 +574,38 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).To(MatchError(ContainSubstring("external Loki endpoint is required"))) }) }) + + Context("When central OTel endpoint is set but password is missing", func() { + BeforeEach(func() { + csEnv.CentralOtelEndpoint = "https://otel.example.com" + csEnv.CentralOtelUsername = "otel-user" + }) + It("returns an error", func() { + err := bs.ValidateInput() + Expect(err).To(MatchError(ContainSubstring("central OTel password is required when central OTel endpoint is set"))) + }) + }) + + Context("When central OTel username is set but password is missing", func() { + BeforeEach(func() { + csEnv.CentralOtelUsername = "otel-user" + }) + It("returns an error", func() { + err := bs.ValidateInput() + Expect(err).To(MatchError(ContainSubstring("central OTel username is set but password is missing"))) + }) + }) + + Context("When central OTel password is set but username is missing", func() { + BeforeEach(func() { + csEnv.CentralOtelPassword = "otel-secret" + csEnv.CentralOtelEndpoint = "https://otel.example.com" + }) + It("returns an error", func() { + err := bs.ValidateInput() + Expect(err).To(MatchError(ContainSubstring("central OTel password is set but username is missing"))) + }) + }) }) Describe("EnsureInstallConfig", func() { diff --git a/internal/bootstrap/gcp/install_config.go b/internal/bootstrap/gcp/install_config.go index 91908827..c437c8bc 100644 --- a/internal/bootstrap/gcp/install_config.go +++ b/internal/bootstrap/gcp/install_config.go @@ -319,6 +319,15 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { } } + if b.Env.CentralOtelPassword != "" || b.Env.LocalTraceExport { + b.Env.InstallConfig.Codesphere.TelemetryExport = &files.TelemetryExport{ + Endpoint: b.Env.CentralOtelEndpoint, + RemoteExport: b.Env.CentralOtelPassword != "", + Traces: b.Env.LocalTraceExport, + SpanMetrics: b.Env.CentralOtelSpanMetrics, + } + } + b.Env.InstallConfig.Codesphere.Experiments = b.Env.Experiments b.Env.InstallConfig.Codesphere.Features = b.Env.FeatureFlags b.applyExternalLokiConfig() diff --git a/internal/bootstrap/gcp/install_config_test.go b/internal/bootstrap/gcp/install_config_test.go index 30156f90..341bfdfe 100644 --- a/internal/bootstrap/gcp/install_config_test.go +++ b/internal/bootstrap/gcp/install_config_test.go @@ -738,6 +738,92 @@ var _ = Describe("Installconfig & Secrets", func() { Expect(loki.User).To(BeEmpty()) }) }) + + Context("When CentralOtelPassword is set", func() { + BeforeEach(func() { + csEnv.CentralOtelPassword = "otel-secret" + csEnv.CentralOtelEndpoint = "https://otel.example.com" + csEnv.CentralOtelSpanMetrics = true + }) + It("sets TelemetryExport with RemoteExport true", func() { + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) + icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() + + err := bs.UpdateInstallConfig() + Expect(err).NotTo(HaveOccurred()) + + te := bs.Env.InstallConfig.Codesphere.TelemetryExport + Expect(te).NotTo(BeNil()) + Expect(te.Endpoint).To(Equal("https://otel.example.com")) + Expect(te.RemoteExport).To(BeTrue()) + Expect(te.Traces).To(BeFalse()) + Expect(te.SpanMetrics).To(BeTrue()) + }) + }) + + Context("When LocalTraceExport is true (no password)", func() { + BeforeEach(func() { + csEnv.LocalTraceExport = true + csEnv.CentralOtelEndpoint = "https://otel.example.com" + }) + It("sets TelemetryExport with Traces true and RemoteExport false", func() { + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) + icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() + + err := bs.UpdateInstallConfig() + Expect(err).NotTo(HaveOccurred()) + + te := bs.Env.InstallConfig.Codesphere.TelemetryExport + Expect(te).NotTo(BeNil()) + Expect(te.Endpoint).To(Equal("https://otel.example.com")) + Expect(te.RemoteExport).To(BeFalse()) + Expect(te.Traces).To(BeTrue()) + Expect(te.SpanMetrics).To(BeFalse()) + }) + }) + + Context("When both CentralOtelPassword and LocalTraceExport are set", func() { + BeforeEach(func() { + csEnv.CentralOtelPassword = "otel-secret" + csEnv.LocalTraceExport = true + csEnv.CentralOtelEndpoint = "https://otel.example.com" + csEnv.CentralOtelSpanMetrics = true + }) + It("sets TelemetryExport with both RemoteExport and Traces true", func() { + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) + icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() + + err := bs.UpdateInstallConfig() + Expect(err).NotTo(HaveOccurred()) + + te := bs.Env.InstallConfig.Codesphere.TelemetryExport + Expect(te).NotTo(BeNil()) + Expect(te.Endpoint).To(Equal("https://otel.example.com")) + Expect(te.RemoteExport).To(BeTrue()) + Expect(te.Traces).To(BeTrue()) + Expect(te.SpanMetrics).To(BeTrue()) + }) + }) + + Context("When neither CentralOtelPassword nor LocalTraceExport are set", func() { + It("leaves TelemetryExport nil", func() { + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) + icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() + + err := bs.UpdateInstallConfig() + Expect(err).NotTo(HaveOccurred()) + + Expect(bs.Env.InstallConfig.Codesphere.TelemetryExport).To(BeNil()) + }) + }) }) Describe("Invalid cases", func() { diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go index 5221e288..55a3fd57 100644 --- a/internal/installer/files/config_yaml.go +++ b/internal/installer/files/config_yaml.go @@ -315,6 +315,7 @@ type CodesphereConfig struct { ManagedServices []ManagedServiceConfig `yaml:"managedServices,omitempty"` OpenBao *OpenBaoConfig `yaml:"openBao,omitempty"` Migration *MigrationConfig `yaml:"migration,omitempty"` + TelemetryExport *TelemetryExport `yaml:"telemetryExport,omitempty"` Override ChartOverride `yaml:"override,omitempty"` DomainAuthPrivateKey string `yaml:"-"` @@ -399,6 +400,13 @@ type FlavorConfig struct { Pool map[int]int `yaml:"pool"` } +type TelemetryExport struct { + Endpoint string `yaml:"endpoint"` + RemoteExport bool `yaml:"remoteExport"` + Traces bool `yaml:"traces"` + SpanMetrics bool `yaml:"spanMetrics"` +} + type ChartOverride = map[string]interface{} type ImageRef struct {