diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index fbf5cf1fec66..3290913ddfa8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -182,8 +182,12 @@ make update # Full update (api-deps, workspace-sync, capi-sync make capi-sync # Sync CAPI provider types (incremental) make capi-sync-force # Force re-sync all CAPI providers make verify-capi-sync # Verify CAPI sync is clean (CI target) +make gmp-sync # Regenerate the pinned GMP API snapshot +make verify-gmp-sync # Verify the GMP snapshot reproduces without changing tracked files ``` +For GMP API updates, see [the package maintenance guide](support/gmpmonitoring/README.md#maintenance). + ## Development Patterns ### Resource Management diff --git a/Makefile b/Makefile index af3905b76d35..fc32eb157d8c 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ build: hypershift-operator control-plane-operator control-plane-pki-operator kar CAPI_PROVIDERS := agent aws azure gcp ibmcloud kubevirt openstack .PHONY: update -update: api-deps workspace-sync capi-sync deps api api-docs clients docs-aggregate +update: api-deps workspace-sync capi-sync gmp-sync deps api api-docs clients docs-aggregate $(MAKE) ownership-rules GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/golangci-lint) @@ -131,7 +131,7 @@ precommit-api-lint-fix: $(GOLANGCI_LINT) cd api && $(GOLANGCI_LINT) fmt --config ./.golangci.yml --enable gci $(patsubst api/%,%,$(FILES)) .PHONY: lint -lint: generate verify-capi-sync $(HYPERSHIFTLINTER_PLUGIN) +lint: generate verify-capi-sync verify-gmp-sync $(HYPERSHIFTLINTER_PLUGIN) $(MAKE) api-lint; api_rc=$$?; \ $(GOLANGCI_LINT) run --config ./.golangci.yml --modules-download-mode=readonly -v; main_rc=$$?; \ exit $$(( api_rc > main_rc ? api_rc : main_rc )) @@ -249,9 +249,17 @@ $(CRD_SCHEMA_CHECK): $(TOOLS_DIR)/go.mod # Build crd-schema-check tool cd $(TOOLS_DIR); $(GO) build -o $(BIN_DIR)/crd-schema-check ./crd-schema-check .PHONY: generate -generate: $(MOCKGEN) +generate: $(MOCKGEN) $(CONTROLLER_GEN) $(GO) generate ./... +.PHONY: gmp-sync +gmp-sync: $(CONTROLLER_GEN) + ./hack/update-gmp-api.sh --sync + +.PHONY: verify-gmp-sync +verify-gmp-sync: $(CONTROLLER_GEN) + ./hack/update-gmp-api.sh --verify + # Compile all tests .PHONY: tests tests: generate @@ -811,7 +819,7 @@ test-commit-layout: $(PYTHON_VENV_STAMP) ## Test the restructure-commits layout .PHONY: verify-codespell verify-codespell: codespell ## Verify codespell. - @$(CODESPELL) --count --ignore-words=./.codespellignore --skip="./hack/tools/bin/codespell_dist,./docs/site/*,./vendor/*,./api/vendor/*,./hack/tools/vendor/*,./api/hypershift/v1alpha1/*,./support/thirdparty/*,./docs/content/reference/*,./hack/tools/bin/*,./cmd/install/assets/*,./go.sum,./api/go.sum,./hack/workspace/go.work.sum,./api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests,./hack/tools/go.mod,./hack/tools/go.sum,./karpenter-operator/controllers/karpenter/assets/*.yaml,./dev/*,./pkg/capi/*,./hack/capi-vendor/*" + @$(CODESPELL) --count --ignore-words=./.codespellignore --skip="./hack/tools/bin/codespell_dist,./docs/site/*,./support/gmpmonitoring/testdata/*,./vendor/*,./api/vendor/*,./hack/tools/vendor/*,./api/hypershift/v1alpha1/*,./support/thirdparty/*,./docs/content/reference/*,./hack/tools/bin/*,./cmd/install/assets/*,./go.sum,./api/go.sum,./hack/workspace/go.work.sum,./api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests,./hack/tools/go.mod,./hack/tools/go.sum,./karpenter-operator/controllers/karpenter/assets/*.yaml,./dev/*,./pkg/capi/*,./hack/capi-vendor/*" .PHONY: verify-api-deps verify-api-deps: $(VERIFY_API_DEPS) ## Verify API dependency allowlist and shared requirements/replacements. diff --git a/OWNERS b/OWNERS index 4f3b306022f8..fb35062ff8fe 100644 --- a/OWNERS +++ b/OWNERS @@ -27,6 +27,11 @@ filters: - gcp-reviewers labels: - area/platform/gcp + "^(support/gmpmonitoring/|support/metrics/gmp_monitoring.*\\.go$|hack/gmp-api-gen/|hack/gmp-api-source/|hack/update-gmp-api\\.sh$)": + reviewers: + - gcp-reviewers + labels: + - area/platform/gcp "^(api|cmd|control-plane-operator|docs|hypershift-operator|product-cli|test)/.*ibmcloud.*": labels: - area/platform/ibmcloud diff --git a/cmd/install/assets/hypershift_operator.go b/cmd/install/assets/hypershift_operator.go index a0922b59caca..a6e5d92285e0 100644 --- a/cmd/install/assets/hypershift_operator.go +++ b/cmd/install/assets/hypershift_operator.go @@ -560,6 +560,7 @@ type HyperShiftOperatorDeployment struct { IncludeVersion bool UWMTelemetry bool RHOBSMonitoring bool + EnableGMPMonitoring bool CVOPrometheusURL string MonitoringDashboards bool CertRotationScale time.Duration @@ -822,6 +823,9 @@ func (o HyperShiftOperatorDeployment) buildArgs() []string { if o.RegistryOverrides != "" { args = append(args, fmt.Sprintf("--registry-overrides=%s", o.RegistryOverrides)) } + if o.EnableGMPMonitoring { + args = append(args, "--enable-gmp-monitoring") + } return args } diff --git a/cmd/install/assets/hypershift_operator_test.go b/cmd/install/assets/hypershift_operator_test.go index 8912253c16e6..1c06986f9932 100644 --- a/cmd/install/assets/hypershift_operator_test.go +++ b/cmd/install/assets/hypershift_operator_test.go @@ -877,6 +877,21 @@ func TestBuildArgs(t *testing.T) { }, expectNotContains: []string{"--registry-overrides"}, }, + { + name: "When GMP monitoring is enabled, it should include the flag in args", + deployment: HyperShiftOperatorDeployment{ + PrivatePlatform: string(hyperv1.NonePlatform), + EnableGMPMonitoring: true, + }, + expectContains: []string{"--enable-gmp-monitoring"}, + }, + { + name: "When GMP monitoring is disabled, it should not include the flag", + deployment: HyperShiftOperatorDeployment{ + PrivatePlatform: string(hyperv1.NonePlatform), + }, + expectNotContains: []string{"--enable-gmp-monitoring"}, + }, { name: "When all standard options are set, it should include them in args", deployment: HyperShiftOperatorDeployment{ diff --git a/cmd/install/install.go b/cmd/install/install.go index 0f767cf405b1..01688b228285 100644 --- a/cmd/install/install.go +++ b/cmd/install/install.go @@ -142,6 +142,7 @@ type Options struct { WaitUntilAvailable bool WaitUntilEstablished bool RHOBSMonitoring bool + EnableGMPMonitoring bool CVOPrometheusURL string SLOsAlerts bool MonitoringDashboards bool @@ -388,6 +389,17 @@ func (o *Options) validateMonitoringConfig() []error { if len(o.CVOPrometheusURL) > 0 && !o.RHOBSMonitoring && !o.EnableCVOManagementClusterMetricsAccess { errs = append(errs, fmt.Errorf("--cvo-prometheus-url requires either --rhobs-monitoring or --enable-cvo-management-cluster-metrics-access to be enabled")) } + // RHOBS can be enabled through the flag or through additional operator env vars; + // match the operator Deployment builder's normalization when checking the GMP conflict. + rhobsMonitoring := o.RHOBSMonitoring + for key, value := range o.AdditionalOperatorEnvVars { + if strings.TrimSpace(key) == rhobsmonitoring.EnvironmentVariable { + rhobsMonitoring = rhobsMonitoring || strings.TrimSpace(value) == "1" + } + } + if err := metrics.ValidateGMPMonitoring(o.EnableGMPMonitoring, rhobsMonitoring); err != nil { + errs = append(errs, fmt.Errorf("--enable-gmp-monitoring: %w", err)) + } return errs } @@ -496,6 +508,7 @@ func NewCommand(clientProviders ...*util.ClientProvider) *cobra.Command { cmd.Flags().BoolVar(&opts.WaitUntilAvailable, "wait-until-available", opts.WaitUntilAvailable, "If true, pauses installation until hypershift operator has been rolled out and its webhook service is available (if installing the webhook)") cmd.Flags().BoolVar(&opts.WaitUntilEstablished, "wait-until-crds-established", opts.WaitUntilEstablished, "If true, pauses installation until all custom resource definitions are established before applying other manifests.") cmd.PersistentFlags().BoolVar(&opts.RHOBSMonitoring, "rhobs-monitoring", opts.RHOBSMonitoring, "If true, HyperShift will generate and use the RHOBS version of monitoring resources (ServiceMonitors, PodMonitors, etc). For ROSA HCP, this also enables the Cluster Version Operator to query the RHOBS Prometheus for conditional update evaluation. Use --cvo-prometheus-url to override the default Prometheus endpoint.") + cmd.PersistentFlags().BoolVar(&opts.EnableGMPMonitoring, "enable-gmp-monitoring", opts.EnableGMPMonitoring, "If true, HyperShift will use Google Managed Prometheus (monitoring.googleapis.com) monitoring resources instead of the default monitoring.coreos.com resources. Cannot be combined with --rhobs-monitoring.") cmd.PersistentFlags().StringVar(&opts.CVOPrometheusURL, "cvo-prometheus-url", opts.CVOPrometheusURL, "Prometheus URL for the Cluster Version Operator to query metrics for conditional update risk evaluation. Only effective when --rhobs-monitoring or --enable-cvo-management-cluster-metrics-access is enabled. If not specified, defaults to the RHOBS monitoring stack for ROSA HCP, or the Thanos querier for self-managed HyperShift.") cmd.PersistentFlags().BoolVar(&opts.SLOsAlerts, "slos-alerts", opts.SLOsAlerts, "If true, HyperShift will generate and use the prometheus alerts for monitoring HostedCluster and NodePools") cmd.PersistentFlags().BoolVar(&opts.MonitoringDashboards, "monitoring-dashboards", opts.MonitoringDashboards, "If true, HyperShift will generate a monitoring dashboard for every HostedCluster that it creates") @@ -1405,6 +1418,7 @@ func setupOperatorResources(opts Options, userCABundleCM *corev1.ConfigMap, trus IncludeVersion: !opts.Template, UWMTelemetry: opts.EnableUWMTelemetryRemoteWrite, RHOBSMonitoring: opts.RHOBSMonitoring, + EnableGMPMonitoring: opts.EnableGMPMonitoring, CVOPrometheusURL: opts.CVOPrometheusURL, MonitoringDashboards: opts.MonitoringDashboards, CertRotationScale: opts.CertRotationScale, diff --git a/cmd/install/install_test.go b/cmd/install/install_test.go index 0608b01e934b..4b2620d6fd6e 100644 --- a/cmd/install/install_test.go +++ b/cmd/install/install_test.go @@ -24,6 +24,7 @@ import ( "github.com/openshift/hypershift/hypershift-operator/controllers/sharedingress" hyperapi "github.com/openshift/hypershift/support/api" "github.com/openshift/hypershift/support/metrics" + "github.com/openshift/hypershift/support/rhobsmonitoring" configv1 "github.com/openshift/api/config/v1" operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1" @@ -1922,9 +1923,10 @@ func TestValidateExternalDNSConfig(t *testing.T) { func TestValidateMonitoringConfig(t *testing.T) { tests := []struct { - name string - opts Options - expectError bool + name string + opts Options + expectError bool + errorContains string }{ { name: "When no monitoring options are set, it should pass", @@ -1954,13 +1956,59 @@ func TestValidateMonitoringConfig(t *testing.T) { }, expectError: false, }, + { + name: "When GMP monitoring is enabled alone, it should pass", + opts: Options{EnableGMPMonitoring: true}, + expectError: false, + }, + { + name: "When GMP monitoring is enabled with the RHOBS flag, it should fail", + opts: Options{EnableGMPMonitoring: true, RHOBSMonitoring: true}, + expectError: true, + errorContains: "--enable-gmp-monitoring", + }, + { + name: "When GMP monitoring is enabled with RHOBS via additional operator env vars, it should fail", + opts: Options{EnableGMPMonitoring: true, AdditionalOperatorEnvVars: map[string]string{" " + rhobsmonitoring.EnvironmentVariable + " ": " 1 "}}, + expectError: true, + errorContains: "--enable-gmp-monitoring", + }, + { + name: "When GMP monitoring is enabled with RHOBS disabled via additional operator env vars, it should pass", + opts: Options{EnableGMPMonitoring: true, AdditionalOperatorEnvVars: map[string]string{rhobsmonitoring.EnvironmentVariable: "0"}}, + expectError: false, + }, + { + name: "When GMP monitoring is enabled with the RHOBS flag and RHOBS disabled via additional operator env vars, it should fail", + opts: Options{EnableGMPMonitoring: true, RHOBSMonitoring: true, AdditionalOperatorEnvVars: map[string]string{rhobsmonitoring.EnvironmentVariable: "0"}}, + expectError: true, + errorContains: "--enable-gmp-monitoring", + }, + { + name: "When GMP monitoring is enabled with a blank RHOBS additional operator env var, it should pass", + opts: Options{EnableGMPMonitoring: true, AdditionalOperatorEnvVars: map[string]string{rhobsmonitoring.EnvironmentVariable: " "}}, + expectError: false, + }, + { + name: "When GMP monitoring is enabled with a non-1 RHOBS additional operator env var, it should pass", + opts: Options{EnableGMPMonitoring: true, AdditionalOperatorEnvVars: map[string]string{rhobsmonitoring.EnvironmentVariable: "true"}}, + expectError: false, + }, + { + name: "When GMP monitoring is enabled with duplicate RHOBS additional operator env vars after trimming, it should fail", + opts: Options{EnableGMPMonitoring: true, AdditionalOperatorEnvVars: map[string]string{rhobsmonitoring.EnvironmentVariable: "0", " " + rhobsmonitoring.EnvironmentVariable: "1"}}, + expectError: true, + errorContains: "--enable-gmp-monitoring", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { g := NewGomegaWithT(t) errs := tc.opts.validateMonitoringConfig() - if tc.expectError { + if tc.errorContains != "" { + g.Expect(errs).To(ContainElement(MatchError(ContainSubstring(tc.errorContains)))) + } else if tc.expectError { g.Expect(errs).NotTo(BeEmpty()) } else { g.Expect(errs).To(BeEmpty()) @@ -2723,6 +2771,53 @@ func TestHyperShiftOperatorManifests_WebhookFlags(t *testing.T) { } } +func TestHyperShiftOperatorManifests_GMPMonitoringFlag(t *testing.T) { + tests := []struct { + name string + enableGMPMonitoring bool + }{ + { + name: "When GMP monitoring is enabled, it should pass the flag to the operator deployment", + enableGMPMonitoring: true, + }, + { + name: "When GMP monitoring is disabled, it should not pass the flag to the operator deployment", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + opts := Options{ + PrivatePlatform: string(hyperv1.NonePlatform), + HyperShiftImage: "test-image", + Namespace: "hypershift", + RenderNamespace: true, + EnableGMPMonitoring: tc.enableGMPMonitoring, + } + + _, objects, err := hyperShiftOperatorManifests(t.Context(), nil, opts) + g.Expect(err).NotTo(HaveOccurred()) + + var deployment *appsv1.Deployment + for _, obj := range objects { + if d, ok := obj.(*appsv1.Deployment); ok { + deployment = d + break + } + } + g.Expect(deployment).NotTo(BeNil(), "should find operator deployment") + + args := deployment.Spec.Template.Spec.Containers[0].Args + if tc.enableGMPMonitoring { + g.Expect(args).To(ContainElement("--enable-gmp-monitoring")) + } else { + g.Expect(args).NotTo(ContainElement("--enable-gmp-monitoring")) + } + }) + } +} + func TestAwsRoleCredentialFileContent(t *testing.T) { tests := map[string]struct { roleARN string diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go index 5b85fbdce490..170cdce5bb57 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go @@ -199,6 +199,7 @@ type HostedControlPlaneReconciler struct { OperateOnReleaseImage string DefaultIngressDomain string MetricsSet metrics.MetricsSet + EnableGMPMonitoring bool KASHealthMetrics *kas.KASHealthMetrics SREConfigHash string ec2Client awsapi.EC2API @@ -1428,6 +1429,7 @@ func (r *HostedControlPlaneReconciler) reconcileCPOV2(ctx context.Context, hcp * SetDefaultSecurityContext: r.SetDefaultSecurityContext, DefaultSecurityContextUID: r.DefaultSecurityContextUID, MetricsSet: r.MetricsSet, + EnableGMPMonitoring: r.EnableGMPMonitoring, EnableCIDebugOutput: r.EnableCIDebugOutput, ImageMetadataProvider: r.ImageMetadataProvider, NativeSidecarContainersEnabled: r.ManagementClusterCapabilities.Has(capabilities.CapabilityNativeSidecarContainers), diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment.go b/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment.go index 52da743072ea..71119ea5b8ed 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment.go @@ -52,6 +52,12 @@ func (cpo *ControlPlaneOperatorOptions) adaptDeployment(cpContext component.Work ) } + // The HostedCluster controller only enables GMP monitoring for CPO images that declare support, + // since older CPO images reject unknown flags. + if cpContext.EnableGMPMonitoring { + c.Args = append(c.Args, "--enable-gmp-monitoring") + } + c.Env = append(c.Env, []corev1.EnvVar{ { Name: "CONTROL_PLANE_OPERATOR_IMAGE", diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment_test.go b/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment_test.go index e5c482662ba7..d528004acff3 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/controlplaneoperator/deployment_test.go @@ -160,3 +160,52 @@ func TestAdaptDeploymentWatchListClientEnv(t *testing.T) { }) } } + +func TestAdaptDeploymentGMPMonitoringArg(t *testing.T) { + testCases := []struct { + name string + enableGMPMonitoring bool + expectPresent bool + }{ + { + name: "When GMP monitoring is enabled, it should pass the flag", + enableGMPMonitoring: true, + expectPresent: true, + }, + { + name: "When GMP monitoring is disabled, it should not pass the flag", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + cpo := &ControlPlaneOperatorOptions{ + HostedCluster: &hyperv1.HostedCluster{}, + Image: "test-image:latest", + UtilitiesImage: "utilities:latest", + HasUtilities: true, + } + + cpContext := controlplanecomponent.WorkloadContext{ + Context: t.Context(), + HCP: &hyperv1.HostedControlPlane{}, + EnableGMPMonitoring: tc.enableGMPMonitoring, + } + + deployment, err := assets.LoadDeploymentManifest(ComponentName) + g.Expect(err).ToNot(HaveOccurred()) + + err = cpo.adaptDeployment(cpContext, deployment) + g.Expect(err).ToNot(HaveOccurred()) + + args := deployment.Spec.Template.Spec.Containers[0].Args + if tc.expectPresent { + g.Expect(args).To(ContainElement("--enable-gmp-monitoring")) + } else { + g.Expect(args).ToNot(ContainElement("--enable-gmp-monitoring")) + } + }) + } +} diff --git a/control-plane-operator/main.go b/control-plane-operator/main.go index 1dee4413d062..8639075d2149 100644 --- a/control-plane-operator/main.go +++ b/control-plane-operator/main.go @@ -44,6 +44,7 @@ import ( "github.com/openshift/hypershift/support/netutil" "github.com/openshift/hypershift/support/podspec" "github.com/openshift/hypershift/support/releaseinfo" + "github.com/openshift/hypershift/support/rhobsmonitoring" "github.com/openshift/hypershift/support/supportedversion" "github.com/openshift/hypershift/support/thirdparty/library-go/pkg/image/reference" "github.com/openshift/hypershift/support/upsert" @@ -337,6 +338,7 @@ func NewStartCommand() *cobra.Command { tokenMinterImage string inCluster bool enableCIDebugOutput bool + enableGMPMonitoring bool registryOverrides map[string]string imageOverridesStr string featureSet string @@ -354,6 +356,7 @@ func NewStartCommand() *cobra.Command { "cluster and will make some internal decisions to ease local development (e.g. using external endpoints where possible"+ "to avoid assuming access to the service network)") cmd.Flags().BoolVar(&enableCIDebugOutput, "enable-ci-debug-output", false, "If extra CI debug output should be enabled") + cmd.Flags().BoolVar(&enableGMPMonitoring, "enable-gmp-monitoring", false, "If true, emit Google Managed Prometheus (monitoring.googleapis.com) monitoring resources instead of monitoring.coreos.com resources") cmd.Flags().StringToStringVar(®istryOverrides, "registry-overrides", map[string]string{}, "registry-overrides contains the source registry string as a key and the destination registry string as value. Images before being applied are scanned for the source registry string and if found the string is replaced with the destination registry string. Format is: sr1=dr1,sr2=dr2") cmd.Flags().StringVar(&imageOverridesStr, "image-overrides", "", "List of images that should be used for a hosted cluster control plane instead of images from OpenShift release specified in HostedCluster. "+ @@ -521,6 +524,12 @@ func NewStartCommand() *cobra.Command { } setupLog.Info("Using metrics set", "set", metricsSet.String()) + if err := metrics.ValidateGMPMonitoring(enableGMPMonitoring, os.Getenv(rhobsmonitoring.EnvironmentVariable) == "1"); err != nil { + setupLog.Error(err, "invalid monitoring configuration") + os.Exit(1) + } + setupLog.Info("Using monitoring configuration", "gmpMonitoring", enableGMPMonitoring) + enableCVOManagementClusterMetricsAccess := (os.Getenv(config.EnableCVOManagementClusterMetricsAccessEnvVar) == "1") if err := (&hostedcontrolplane.HostedControlPlaneReconciler{ @@ -533,6 +542,7 @@ func NewStartCommand() *cobra.Command { OperateOnReleaseImage: os.Getenv("OPERATE_ON_RELEASE_IMAGE"), DefaultIngressDomain: defaultIngressDomain, MetricsSet: metricsSet, + EnableGMPMonitoring: enableGMPMonitoring, KASHealthMetrics: kas.NewKASHealthMetrics(crmetrics.Registry), CertRotationScale: certRotationScale, EnableCVOManagementClusterMetricsAccess: enableCVOManagementClusterMetricsAccess, diff --git a/go.mod b/go.mod index cf03e076c548..bf24f9490277 100644 --- a/go.mod +++ b/go.mod @@ -191,8 +191,8 @@ require ( github.com/go-openapi/jsonreference v1.0.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/spec v0.21.0 + github.com/go-openapi/strfmt v0.25.0 github.com/go-openapi/swag v0.27.1 // indirect github.com/go-openapi/swag/cmdutils v0.27.1 // indirect github.com/go-openapi/swag/conv v0.27.1 // indirect @@ -205,7 +205,7 @@ require ( github.com/go-openapi/swag/stringutils v0.27.1 // indirect github.com/go-openapi/swag/typeutils v0.27.1 // indirect github.com/go-openapi/swag/yamlutils v0.27.1 // indirect - github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-openapi/validate v0.24.0 github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.28.0 // indirect diff --git a/hack/gmp-api-gen/main.go b/hack/gmp-api-gen/main.go new file mode 100644 index 000000000000..3a148b5dea11 --- /dev/null +++ b/hack/gmp-api-gen/main.go @@ -0,0 +1,255 @@ +// gmp-api-gen extracts the data-type closure of the supported GMP resources. +// Runtime methods, interfaces and unrelated resources are deliberately excluded. +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strconv" + "strings" +) + +func main() { + upstream := flag.String("upstream", "", "pinned prometheus-engine source directory") + output := flag.String("output", "support/gmpmonitoring", "output directory") + sourceInfo := flag.String("source-info", "", "go mod download JSON for generated provenance") + flag.Parse() + if err := generate(*upstream, *output); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if *sourceInfo != "" { + if err := writeProvenance(*upstream, *sourceInfo, *output); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + } +} + +func generate(upstream, output string) error { + files, err := apiSourceFiles(upstream) + if err != nil { + return err + } + fset := token.NewFileSet() + types := map[string]*ast.GenDecl{} + sources := map[*ast.GenDecl][]byte{} + imports := map[*ast.GenDecl]map[string]string{} + packageOffsets := map[*ast.GenDecl]int{} + var condition *ast.GenDecl + for _, name := range files { + path := filepath.Join(upstream, name) + source, err := os.ReadFile(path) + if err != nil { + return err + } + file, err := parser.ParseFile(fset, path, source, parser.ParseComments) + if err != nil { + return err + } + if file.Name.Name != "v1" { + return fmt.Errorf("unexpected upstream package %q in %s", file.Name.Name, name) + } + fileImports := map[string]string{} + for _, imp := range file.Imports { + path, err := strconv.Unquote(imp.Path.Value) + if err != nil { + return err + } + alias := filepath.Base(path) + if imp.Name != nil { + alias = imp.Name.Name + } + fileImports[alias] = path + } + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || (gen.Tok != token.TYPE && gen.Tok != token.CONST) { + continue + } + sources[gen] = source + imports[gen] = fileImports + packageOffsets[gen] = fset.Position(file.Package).Offset + for _, spec := range gen.Specs { + if typ, ok := spec.(*ast.TypeSpec); ok { + types[typ.Name.Name] = gen + } + if val, ok := spec.(*ast.ValueSpec); ok && val.Names[0].Name == "ConfigurationCreateSuccess" { + condition = gen + } + } + } + } + names, err := selectTypes(types) + if err != nil { + return err + } + usedImports := map[string]string{} + var body bytes.Buffer + writeDecl := func(decl *ast.GenDecl) error { + if err := collectImports(decl, imports[decl], usedImports); err != nil { + return err + } + start := decl.Pos() + if decl.Doc != nil { + start = decl.Doc.Pos() + } + source := sources[decl][fset.Position(start).Offset:fset.Position(decl.End()).Offset] + if err := rejectDirectives(source, fset.Position(start).String()); err != nil { + return err + } + body.Write(source) + body.WriteString("\n\n") + return nil + } + if condition == nil { + return fmt.Errorf("missing upstream monitoring condition constant") + } + if err := writeDecl(condition); err != nil { + return err + } + for _, name := range names { + if err := writeDecl(types[name]); err != nil { + return err + } + } + var result bytes.Buffer + // Preserve the upstream license header and make provenance explicit. + licenseHeader := sources[types["PodMonitoring"]][:packageOffsets[types["PodMonitoring"]]] + if err := rejectDirectives(licenseHeader, "license header of "+fset.Position(types["PodMonitoring"].Pos()).Filename); err != nil { + return err + } + result.Write(licenseHeader) + result.WriteString("// Code generated by hack/update-gmp-api.sh; DO NOT EDIT.\n\npackage gmpmonitoring\n\nimport (\n") + aliases := make([]string, 0, len(usedImports)) + for alias := range usedImports { + aliases = append(aliases, alias) + } + sort.Strings(aliases) + for _, alias := range aliases { + path := usedImports[alias] + fmt.Fprintf(&result, "%s %q\n", alias, path) + } + result.WriteString(")\n\n") + result.Write(body.Bytes()) + formatted, err := format.Source(result.Bytes()) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(output, "types.go"), formatted, 0644) +} + +// rejectDirectives fails when copied upstream source contains Go tool or compiler directives. +// The snapshot is API data only, so directives such as go:generate or go:build must never be copied. +func rejectDirectives(source []byte, location string) error { + for _, line := range strings.Split(string(source), "\n") { + line = strings.TrimSpace(line) + for _, prefix := range []string{"//go:", "// +build", "//line ", "/*line ", "//export "} { + if strings.HasPrefix(line, prefix) { + return fmt.Errorf("upstream %s contains Go directive %q; review the upstream change", location, line) + } + } + } + return nil +} + +func collectImports(decl *ast.GenDecl, imports, usedImports map[string]string) error { + var importError error + ast.Inspect(decl, func(node ast.Node) bool { + if selector, ok := node.(*ast.SelectorExpr); ok { + if alias, ok := selector.X.(*ast.Ident); ok { + path := imports[alias.Name] + switch path { + case "github.com/prometheus/common/model", "k8s.io/api/core/v1", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/util/intstr": + default: + importError = fmt.Errorf("unsupported API import %q for %s; review the upstream data dependency", path, alias.Name) + return false + } + if previous := usedImports[alias.Name]; previous != "" && previous != path { + importError = fmt.Errorf("conflicting upstream import alias %s: %s and %s", alias.Name, previous, path) + return false + } + usedImports[alias.Name] = path + } + } + return true + }) + return importError +} + +func selectTypes(types map[string]*ast.GenDecl) ([]string, error) { + selected := map[string]bool{} + var visit func(string) error + visit = func(name string) error { + if selected[name] { + return nil + } + decl, ok := types[name] + if !ok { + return fmt.Errorf("missing upstream type %s", name) + } + if len(decl.Specs) != 1 { + return fmt.Errorf("unsupported grouped upstream type declaration for %s", name) + } + spec := decl.Specs[0].(*ast.TypeSpec) + if _, ok := spec.Type.(*ast.InterfaceType); ok { + return fmt.Errorf("API data unexpectedly depends on interface %s", name) + } + selected[name] = true + var err error + var inspect func(ast.Node) bool + inspect = func(node ast.Node) bool { + switch node := node.(type) { + case *ast.Field: + ast.Inspect(node.Type, inspect) + return false // Field names are declarations, not type references. + case *ast.SelectorExpr: + return false // External types are imports, not part of the local closure. + case *ast.InterfaceType: + err = fmt.Errorf("API data unexpectedly depends on interface in %s", name) + return false + } + if id, ok := node.(*ast.Ident); ok && types[id.Name] != nil && err == nil { + err = visit(id.Name) + } + return err == nil + } + ast.Inspect(spec.Type, inspect) + return err + } + for _, name := range []string{"PodMonitoring", "PodMonitoringList", "Rules", "RulesList"} { + if err := visit(name); err != nil { + return nil, err + } + } + names := make([]string, 0, len(selected)) + for name := range selected { + names = append(names, name) + } + sort.Strings(names) + return names, nil +} + +func apiSourceFiles(upstream string) ([]string, error) { + const directory = "pkg/operator/apis/monitoring/v1" + entries, err := os.ReadDir(filepath.Join(upstream, directory)) + if err != nil { + return nil, err + } + var files []string + for _, entry := range entries { + name := entry.Name() + if !entry.IsDir() && strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") && !strings.HasPrefix(name, "zz_generated.") { + files = append(files, filepath.Join(directory, name)) + } + } + return files, nil +} diff --git a/hack/gmp-api-gen/main_test.go b/hack/gmp-api-gen/main_test.go new file mode 100644 index 000000000000..1e008a43b127 --- /dev/null +++ b/hack/gmp-api-gen/main_test.go @@ -0,0 +1,255 @@ +package main + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSelectTypes(t *testing.T) { + for _, tc := range []struct{ name, source, wantError string }{ + {name: "When API types contain recursive references, it should terminate and return a sorted closure", source: `package v1 +type PodMonitoring struct { Ref *PodMonitoring } +type PodMonitoringList struct { Items []PodMonitoring } +type Rules struct { Ref *PodMonitoring } +type RulesList struct { Items []Rules } +type Unrelated string`}, + {name: "When a field name matches an unrelated type, it should not add that type to the closure", source: `package v1 +type PodMonitoring struct { Authorization string } +type PodMonitoringList struct { Items []PodMonitoring } +type Rules struct{} +type RulesList struct { Items []Rules } +type Authorization interface { Reconcile() }`}, + {name: "When a required root type is missing, it should fail explicitly", source: "package v1", wantError: "missing upstream type PodMonitoring"}, + {name: "When a selected type uses a grouped declaration, it should require explicit generator support", source: `package v1 +type (PodMonitoring struct{}; PodMonitoringList struct{}) +type Rules struct{} +type RulesList struct{}`, wantError: "unsupported grouped upstream type declaration"}, + } { + t.Run(tc.name, func(t *testing.T) { + file, err := parser.ParseFile(token.NewFileSet(), "types.go", tc.source, 0) + if err != nil { + t.Fatal(err) + } + types := map[string]*ast.GenDecl{} + for _, node := range file.Decls { + decl := node.(*ast.GenDecl) + for _, spec := range decl.Specs { + types[spec.(*ast.TypeSpec).Name.Name] = decl + } + } + names, err := selectTypes(types) + if tc.wantError != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("expected %q, got %v", tc.wantError, err) + } + return + } + if err != nil { + t.Fatal(err) + } + if strings.Join(names, ",") != "PodMonitoring,PodMonitoringList,Rules,RulesList" { + t.Fatalf("unexpected closure %v", names) + } + }) + } +} + +func TestCollectImports(t *testing.T) { + for _, tc := range []struct { + name, path, wantError string + }{ + {name: "When a selected declaration uses Kubernetes metadata, it should collect only that import", path: "k8s.io/apimachinery/pkg/apis/meta/v1"}, + {name: "When a selected declaration imports operator code, it should reject the dependency", path: "coupled/operator", wantError: "unsupported API import"}, + } { + t.Run(tc.name, func(t *testing.T) { + file, err := parser.ParseFile(token.NewFileSet(), "types.go", "package v1\ntype Snapshot struct { Metadata meta.ObjectMeta }", 0) + if err != nil { + t.Fatal(err) + } + used := map[string]string{} + err = collectImports(file.Decls[0].(*ast.GenDecl), map[string]string{"meta": tc.path, "unused": "coupled/unused"}, used) + if tc.wantError != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("expected %q, got %v", tc.wantError, err) + } + return + } + if err != nil { + t.Fatal(err) + } + if len(used) != 1 || used["meta"] != tc.path { + t.Fatalf("unexpected imports: %v", used) + } + }) + } +} + +func TestAPISourceFiles(t *testing.T) { + for _, tc := range []struct { + name string + missing bool + }{ + {name: "When the source directory contains mixed inputs, it should select sorted non-generated Go sources"}, + {name: "When the upstream API directory is missing, it should report the error", missing: true}, + } { + t.Run(tc.name, func(t *testing.T) { + upstream := t.TempDir() + directory := filepath.Join(upstream, "pkg/operator/apis/monitoring/v1") + if !tc.missing { + if err := os.MkdirAll(directory, 0755); err != nil { + t.Fatal(err) + } + for _, name := range []string{"z.go", "a.go", "a_test.go", "zz_generated.deepcopy.go", "README.md"} { + if err := os.WriteFile(filepath.Join(directory, name), nil, 0644); err != nil { + t.Fatal(err) + } + } + if err := os.Mkdir(filepath.Join(directory, "nested.go"), 0755); err != nil { + t.Fatal(err) + } + } + files, err := apiSourceFiles(upstream) + if tc.missing { + if !os.IsNotExist(err) { + t.Fatalf("expected a missing directory error, got %v", err) + } + return + } + if err != nil { + t.Fatal(err) + } + want := "pkg/operator/apis/monitoring/v1/a.go,pkg/operator/apis/monitoring/v1/z.go" + if strings.Join(files, ",") != want { + t.Fatalf("unexpected source selection: %v", files) + } + }) + } +} + +func TestGenerate(t *testing.T) { + for _, tc := range []struct { + name, nested, nestedImports, podHeader, wantError string + extraFiles map[string]string + omitCondition bool + }{ + {name: "When nested data moves into another source file, it should copy only the data closure deterministically", nested: "struct { Labels map[string]string; Values []string }"}, + {name: "When API data depends on a runtime interface, it should reject the dependency", nested: "interface { Reconcile() }", wantError: "unexpectedly depends on interface"}, + {name: "When API data contains an anonymous runtime interface, it should reject the dependency", nested: "struct { Runtime interface { Reconcile() } }", wantError: "unexpectedly depends on interface"}, + {name: "When a selected type imports runtime code, it should reject the dependency", nested: "struct { Client operator.Client }", nestedImports: `import "coupled/operator"`, wantError: `unsupported API import "coupled/operator"`}, + {name: "When an unrelated file reuses an import alias, it should retain the selected type's import", nested: "struct { Label model.LabelName }", nestedImports: `import "github.com/prometheus/common/model"`, extraFiles: map[string]string{"unrelated.go": "package v1\nimport model \"coupled/operator\"\ntype Ignored struct { Client model.Client }"}}, + {name: "When selected declarations conflict on an import alias, it should report the conflict", nested: "struct { Ref shared.SecretReference }", nestedImports: `import shared "k8s.io/api/core/v1"`, extraFiles: map[string]string{"rules_types.go": "package v1\nimport shared \"k8s.io/apimachinery/pkg/apis/meta/v1\"\ntype Rules struct { Selector shared.LabelSelector }\ntype RulesList struct { Items []Rules }"}, wantError: "conflicting upstream import alias shared"}, + {name: "When the upstream condition constant is removed, it should report the missing declaration", nested: "string", omitCondition: true, wantError: "missing upstream monitoring condition constant"}, + {name: "When a source file belongs to another package, it should fail explicitly", nested: "string", extraFiles: map[string]string{"other.go": "package other"}, wantError: "unexpected upstream package"}, + {name: "When a selected type's documentation contains a go:generate directive, it should reject the directive", nested: "string", extraFiles: map[string]string{"transport_types.go": "package v1\n// Nested data.\n//go:generate touch generated\ntype Nested string"}, wantError: `contains Go directive "//go:generate touch generated"`}, + {name: "When a selected type's field comment contains a go:generate directive, it should reject the directive", nested: "struct {\n\t//go:generate touch generated\n\tValue string\n}", wantError: "contains Go directive"}, + {name: "When the upstream license header contains a build constraint, it should reject the directive", nested: "string", podHeader: "//go:build ignore\n\n", wantError: "license header"}, + } { + t.Run(tc.name, func(t *testing.T) { + upstream, output := t.TempDir(), t.TempDir() + input := filepath.Join(upstream, "pkg/operator/apis/monitoring/v1") + if err := os.MkdirAll(input, 0755); err != nil { + t.Fatal(err) + } + files := map[string]string{ + "pod_types.go": tc.podHeader + `// Copyright Google LLC +package v1 +import "coupled/operator" +// PodMonitoring carries monitoring data. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type PodMonitoring struct { Spec *Nested } +type PodMonitoringList struct { Items []PodMonitoring } +type ClusterPodMonitoring struct { Runtime operator.Client } +func (*PodMonitoring) Runtime() { operator.Reconcile() }`, + "transport_types.go": "package v1\n" + tc.nestedImports + "\ntype Nested " + tc.nested, + "rules_types.go": `package v1 +type Rules struct { State MonitoringConditionType } +type RulesList struct { Items []Rules } +type ClusterRules struct { Runtime string }`, + "http_types.go": `package v1 +type Unrelated struct { Runtime string }`, + "monitoring_types.go": `package v1 +type MonitoringConditionType string +const ( // Monitoring configuration condition. +ConfigurationCreateSuccess MonitoringConditionType = "ConfigurationCreateSuccess" +)`, + "ignored_test.go": "not valid Go source", + "zz_generated.deepcopy.go": "not valid Go source", + } + if tc.omitCondition { + files["monitoring_types.go"] = "package v1\ntype MonitoringConditionType string" + } + for name, source := range tc.extraFiles { + files[name] = source + } + for name, source := range files { + if err := os.WriteFile(filepath.Join(input, name), []byte(source), 0644); err != nil { + t.Fatal(err) + } + } + err := generate(upstream, output) + if tc.wantError != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("expected %q, got %v", tc.wantError, err) + } + return + } + if err != nil { + t.Fatal(err) + } + first, err := os.ReadFile(filepath.Join(output, "types.go")) + if err != nil { + t.Fatal(err) + } + for _, required := range []string{"Copyright Google LLC", "type Nested", "type PodMonitoringList", "type RulesList", "ConfigurationCreateSuccess", "+k8s:deepcopy-gen"} { + if !strings.Contains(string(first), required) { + t.Fatalf("missing %q", required) + } + } + for _, forbidden := range []string{"coupled/operator", "func (", "ClusterPodMonitoring", "ClusterRules", "Unrelated"} { + if strings.Contains(string(first), forbidden) { + t.Fatalf("unexpected runtime or unrelated declaration %q", forbidden) + } + } + if err := generate(upstream, output); err != nil { + t.Fatal(err) + } + second, err := os.ReadFile(filepath.Join(output, "types.go")) + if err != nil { + t.Fatal(err) + } + if string(first) != string(second) { + t.Fatal("generation changed on identical inputs") + } + }) + } +} + +func TestRejectDirectives(t *testing.T) { + for _, tc := range []struct{ name, source, wantError string }{ + {name: "When source contains only documentation and API markers, it should accept it", source: "// PodMonitoring carries data.\n// +kubebuilder:object:root=true\n// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object\ntype PodMonitoring struct{}"}, + {name: "When source contains a go:generate directive, it should reject it", source: "//go:generate touch generated", wantError: "//go:generate"}, + {name: "When source contains an indented go:generate directive, it should reject it", source: "type T struct {\n\t//go:generate touch generated\n}", wantError: "//go:generate"}, + {name: "When source contains a go:build constraint, it should reject it", source: "//go:build ignore", wantError: "//go:build"}, + {name: "When source contains a legacy build constraint, it should reject it", source: "// +build ignore", wantError: "// +build"}, + {name: "When source contains a line directive, it should reject it", source: "//line generated.go:1", wantError: "//line"}, + {name: "When source contains a cgo export directive, it should reject it", source: "//export Generated", wantError: "//export"}, + } { + t.Run(tc.name, func(t *testing.T) { + err := rejectDirectives([]byte(tc.source), "types.go") + if tc.wantError != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("expected %q, got %v", tc.wantError, err) + } + return + } + if err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/hack/gmp-api-gen/provenance.go b/hack/gmp-api-gen/provenance.go new file mode 100644 index 000000000000..d2f62c924dfb --- /dev/null +++ b/hack/gmp-api-gen/provenance.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" +) + +func writeProvenance(upstream, sourceInfo, output string) error { + data, err := os.ReadFile(sourceInfo) + if err != nil { + return err + } + var info struct { + Path, Version, Sum, GoModSum string + Origin struct{ Hash string } + } + if err := json.Unmarshal(data, &info); err != nil { + return fmt.Errorf("decode upstream module information: %w", err) + } + if info.Path != "github.com/GoogleCloudPlatform/prometheus-engine" || info.Version == "" || info.Sum == "" || info.GoModSum == "" { + return fmt.Errorf("incomplete or unexpected upstream module information") + } + if decoded, err := hex.DecodeString(info.Origin.Hash); err != nil || len(decoded) != 20 { + return fmt.Errorf("upstream module information must include a full Git revision") + } + files, err := apiSourceFiles(upstream) + if err != nil { + return err + } + files = append(files, + "charts/operator/crds/monitoring.googleapis.com_podmonitorings.yaml", + "charts/operator/crds/monitoring.googleapis.com_rules.yaml") + sort.Strings(files) + var manifest bytes.Buffer + fmt.Fprintf(&manifest, "# Code generated by hack/update-gmp-api.sh; DO NOT EDIT.\n# Module: %s\n# Version: %s\n# Revision: %s\n# Module sum: %s\n# go.mod sum: %s\n", info.Path, info.Version, info.Origin.Hash, info.Sum, info.GoModSum) + for _, name := range files { + contents, err := os.ReadFile(filepath.Join(upstream, name)) + if err != nil { + return err + } + fmt.Fprintf(&manifest, "%x %s\n", sha256.Sum256(contents), filepath.ToSlash(name)) + } + return os.WriteFile(filepath.Join(output, "upstream.sha256"), manifest.Bytes(), 0644) +} diff --git a/hack/gmp-api-gen/provenance_test.go b/hack/gmp-api-gen/provenance_test.go new file mode 100644 index 000000000000..6b5e0dabedeb --- /dev/null +++ b/hack/gmp-api-gen/provenance_test.go @@ -0,0 +1,157 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +type provenanceCase struct { + name, field, value, remove, wantError string +} + +func TestWriteProvenance(t *testing.T) { + for _, tc := range []provenanceCase{ + {name: "When the source is pinned, it should record deterministic provenance and detect changed input contents"}, + {name: "When the source module is unexpected, it should reject the metadata", field: "Path", value: "github.com/openshift/hypershift", wantError: "unexpected upstream module"}, + {name: "When the version is missing, it should reject incomplete metadata", field: "Version", wantError: "incomplete"}, + {name: "When the module checksum is missing, it should reject incomplete metadata", field: "Sum", wantError: "incomplete"}, + {name: "When the module-file checksum is missing, it should reject incomplete metadata", field: "GoModSum", wantError: "incomplete"}, + {name: "When the revision is missing, it should reject incomplete metadata", field: "Hash", wantError: "full Git revision"}, + {name: "When the revision is malformed, it should reject the metadata", field: "Hash", value: "not-a-git-revision", wantError: "full Git revision"}, + {name: "When a CRD fixture is missing, it should fail before writing provenance", remove: "charts/operator/crds/monitoring.googleapis.com_rules.yaml", wantError: "no such file"}, + } { + t.Run(tc.name, func(t *testing.T) { testWriteProvenanceCase(t, tc) }) + } + + // The committed provenance must match the pinned source module, so a pin change without + // make gmp-sync (for example an automated dependency update) fails offline unit tests. + t.Run("When the committed snapshot is checked, it should match the pinned source module", testWriteProvenancePinnedSnapshot) +} + +func testWriteProvenanceCase(t *testing.T, tc provenanceCase) { + upstream, output := t.TempDir(), t.TempDir() + for _, name := range []string{ + "pkg/operator/apis/monitoring/v1/pod_types.go", + "pkg/operator/apis/monitoring/v1/transport_types.go", + "pkg/operator/apis/monitoring/v1/ignored_test.go", + "charts/operator/crds/monitoring.googleapis.com_podmonitorings.yaml", + "charts/operator/crds/monitoring.googleapis.com_rules.yaml", + } { + if name == tc.remove { + continue + } + path := filepath.Join(upstream, name) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("source"), 0644); err != nil { + t.Fatal(err) + } + } + info := map[string]interface{}{ + "Path": "github.com/GoogleCloudPlatform/prometheus-engine", "Version": "v0.17.2", + "Sum": "h1:module", "GoModSum": "h1:module-file", + "Origin": map[string]string{"Hash": "058752ec791edefba5c42486eb9237562c3f0d29"}, + } + if tc.field == "Hash" { + info["Origin"].(map[string]string)["Hash"] = tc.value + } else if tc.field != "" { + info[tc.field] = tc.value + } + data, err := json.Marshal(info) + if err != nil { + t.Fatal(err) + } + sourceInfo := filepath.Join(t.TempDir(), "source.json") + if err := os.WriteFile(sourceInfo, data, 0644); err != nil { + t.Fatal(err) + } + err = writeProvenance(upstream, sourceInfo, output) + if tc.wantError != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("expected %q, got %v", tc.wantError, err) + } + if _, err := os.Stat(filepath.Join(output, "upstream.sha256")); !os.IsNotExist(err) { + t.Fatal("failed generation should not write provenance") + } + return + } + if err != nil { + t.Fatal(err) + } + manifest := filepath.Join(output, "upstream.sha256") + first, err := os.ReadFile(manifest) + if err != nil { + t.Fatal(err) + } + for _, required := range []string{"# Version: v0.17.2", "# Revision: 058752ec791edefba5c42486eb9237562c3f0d29", "# Module sum: h1:module", "transport_types.go", "monitoring.googleapis.com_rules.yaml"} { + if !strings.Contains(string(first), required) { + t.Fatalf("missing provenance %q", required) + } + } + if strings.Contains(string(first), "ignored_test.go") { + t.Fatal("upstream tests are not extraction inputs") + } + if err := writeProvenance(upstream, sourceInfo, output); err != nil { + t.Fatal(err) + } + second, err := os.ReadFile(manifest) + if err != nil { + t.Fatal(err) + } + if string(first) != string(second) { + t.Fatal("provenance changed on identical inputs") + } + if err := os.WriteFile(filepath.Join(upstream, "pkg/operator/apis/monitoring/v1/transport_types.go"), []byte("changed"), 0644); err != nil { + t.Fatal(err) + } + if err := writeProvenance(upstream, sourceInfo, output); err != nil { + t.Fatal(err) + } + third, err := os.ReadFile(manifest) + if err != nil { + t.Fatal(err) + } + if string(first) == string(third) { + t.Fatal("changed input contents did not change the recorded checksum") + } +} + +func testWriteProvenancePinnedSnapshot(t *testing.T) { + const module = "github.com/GoogleCloudPlatform/prometheus-engine" + read := func(path string) string { + data, err := os.ReadFile(filepath.Join("..", "..", path)) + if err != nil { + t.Fatal(err) + } + return string(data) + } + var versions []string + for _, line := range strings.Split(read("hack/gmp-api-source/go.mod"), "\n") { + if fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require ")); len(fields) >= 2 && fields[0] == module { + versions = append(versions, fields[1]) + } + } + if len(versions) != 1 { + t.Fatalf("hack/gmp-api-source/go.mod must require exactly one %s version, got %v; do not run go mod tidy in this download-only module", module, versions) + } + sums := map[string]string{} + for _, line := range strings.Split(read("hack/gmp-api-source/go.sum"), "\n") { + if fields := strings.Fields(line); len(fields) == 3 && fields[0] == module { + sums[fields[1]] = fields[2] + } + } + provenance := read("support/gmpmonitoring/upstream.sha256") + for _, want := range []string{ + "# Version: " + versions[0] + "\n", + "# Module sum: " + sums[versions[0]] + "\n", + "# go.mod sum: " + sums[versions[0]+"/go.mod"] + "\n", + } { + if !strings.Contains(provenance, want) { + t.Fatalf("support/gmpmonitoring/upstream.sha256 does not match hack/gmp-api-source (missing %q); run make gmp-sync", strings.TrimSpace(want)) + } + } +} diff --git a/hack/gmp-api-source/go.mod b/hack/gmp-api-source/go.mod new file mode 100644 index 000000000000..7b379e2a9bd1 --- /dev/null +++ b/hack/gmp-api-source/go.mod @@ -0,0 +1,6 @@ +module github.com/openshift/hypershift/hack/gmp-api-source + +go 1.26.3 + +// Download-only input for GMP API generation. Do not run go mod tidy here. +require github.com/GoogleCloudPlatform/prometheus-engine v0.17.2 diff --git a/hack/gmp-api-source/go.sum b/hack/gmp-api-source/go.sum new file mode 100644 index 000000000000..49b4c2e254d6 --- /dev/null +++ b/hack/gmp-api-source/go.sum @@ -0,0 +1,2 @@ +github.com/GoogleCloudPlatform/prometheus-engine v0.17.2 h1:b4Sau/04Xn0wqGyGABUzLVOSlM7BxsZHbcYEXP3EDeg= +github.com/GoogleCloudPlatform/prometheus-engine v0.17.2/go.mod h1:Mfy+yPZh97JLpnp8/WADyS3PJ3xzK8tua/luLRYkwtU= diff --git a/hack/update-gmp-api.sh b/hack/update-gmp-api.sh new file mode 100755 index 000000000000..16e7131dbde4 --- /dev/null +++ b/hack/update-gmp-api.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode="${1:---sync}" +if [[ $# -gt 1 || ( "$mode" != --sync && "$mode" != --verify ) ]]; then + echo 'usage: hack/update-gmp-api.sh [--sync|--verify]' >&2 + exit 2 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source_module="$repo_root/hack/gmp-api-source" +target="$repo_root/support/gmpmonitoring" +scratch_dir="$(mktemp -d)" +candidate_dir="" +trap 'rm -rf "$scratch_dir"; if [[ -n "$candidate_dir" ]]; then rm -rf "$candidate_dir"; fi' EXIT + +# Download in a disposable module so verification cannot update tracked pins. +cp "$source_module/go.mod" "$scratch_dir/go.mod" +if [[ -f "$source_module/go.sum" ]]; then + cp "$source_module/go.sum" "$scratch_dir/go.sum" +fi +(cd "$scratch_dir" && GO111MODULE=on GOWORK=off GOFLAGS= go mod edit -json) > "$scratch_dir/pin.json" +upstream_version="$(python3 - "$scratch_dir/pin.json" <<'PY' +import json, sys +with open(sys.argv[1]) as source: + requirements = json.load(source).get('Require', []) +versions = [item['Version'] for item in requirements if item['Path'] == 'github.com/GoogleCloudPlatform/prometheus-engine'] +if len(versions) != 1: + raise SystemExit('source module must pin exactly one prometheus-engine version') +print(versions[0]) +PY +)" +# Always authenticate the download against the public checksum database, even if the +# caller's environment exempts modules from it (GOPRIVATE, GONOSUMDB, GOSUMDB=off). +if ! (cd "$scratch_dir" && GO111MODULE=on GOWORK=off GOFLAGS= GOSUMDB=sum.golang.org GONOSUMDB= GOPRIVATE= GOINSECURE= \ + go mod download -json "github.com/GoogleCloudPlatform/prometheus-engine@$upstream_version") > "$scratch_dir/source.json"; then + cat "$scratch_dir/source.json" >&2 + exit 1 +fi +upstream_dir="$(python3 - "$scratch_dir/source.json" <<'PY' +import json, pathlib, sys +with open(sys.argv[1]) as source: + info = json.load(source) +directory = info.get('Dir', '') +if info.get('Error') or not directory or not pathlib.Path(directory).is_dir(): + raise SystemExit('failed to resolve the pinned GMP source module') +print(directory) +PY +)" +if ! cmp -s "$source_module/go.mod" "$scratch_dir/go.mod"; then + echo 'The source module changed during download; review its Go version before syncing.' >&2 + exit 1 +fi + +# A hidden package uses root vendoring without entering recursive package scans. +candidate_dir="$(mktemp -d "$repo_root/.gmp-api-gen.XXXXXX")" +cp "$target/doc.go" "$target/scheme.go" "$candidate_dir/" +cd "$repo_root" +GO111MODULE=on GOWORK=off GOFLAGS=-mod=vendor go run ./hack/gmp-api-gen \ + --upstream "$upstream_dir" --output "$candidate_dir" --source-info "$scratch_dir/source.json" +mkdir "$candidate_dir/testdata" +for resource in podmonitorings rules; do + cp "$upstream_dir/charts/operator/crds/monitoring.googleapis.com_$resource.yaml" "$candidate_dir/testdata/$resource.crd.yaml" +done +make "$repo_root/hack/tools/bin/controller-gen" +GO111MODULE=on GOWORK=off GOFLAGS=-mod=vendor hack/tools/bin/controller-gen \ + object:headerFile=hack/boilerplate.go.txt paths="./$(basename "$candidate_dir")" + +python3 - "$mode" "$target" "$candidate_dir" "$source_module" "$scratch_dir" <<'PY' +import pathlib, shutil, sys + +mode = sys.argv[1] +target, candidate, source_module, scratch = map(pathlib.Path, sys.argv[2:]) +outputs = { + 'types.go', 'zz_generated.deepcopy.go', 'upstream.sha256', + 'testdata/podmonitorings.crd.yaml', 'testdata/rules.crd.yaml', +} +existing = { + str(path.relative_to(target)) + for path in (target / 'testdata').rglob('*') if path.is_file() +} +for path in target.glob('*.go'): + if path.name.startswith('zz_generated.') or b'// Code generated ' in path.read_bytes()[:2048]: + existing.add(path.name) +unexpected = sorted(existing - outputs) +if unexpected: + raise SystemExit('Unexpected generated files; ask gcp-reviewers to review: ' + ', '.join(unexpected)) +missing = sorted(name for name in outputs if not (candidate / name).is_file()) +if missing: + raise SystemExit('Generation did not produce expected files: ' + ', '.join(missing)) + +pairs = [(candidate / name, target / name) for name in sorted(outputs)] +if not (scratch / 'go.sum').is_file(): + raise SystemExit('Pinned source download did not produce go.sum; cannot record module checksums.') +pairs.append((scratch / 'go.sum', source_module / 'go.sum')) +if mode == '--verify': + differences = [str(dst) for src, dst in pairs if not dst.is_file() or src.read_bytes() != dst.read_bytes()] + if differences: + raise SystemExit('GMP API files do not match the pinned source:\n' + '\n'.join(differences) + + '\nRun make gmp-sync, review the generated changes, and commit them.') + print('GMP API snapshot matches the pinned source.') +else: + for src, dst in pairs: + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src, dst) + print('GMP API snapshot synchronized. Run make verify-gmp-sync and the focused tests.') +PY diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index 0f01306a49f5..bdf882dad382 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -138,6 +138,7 @@ const ( controlPlaneOperatorSubcommandsLabel = "io.openshift.hypershift.control-plane-operator-subcommands" controlPlaneOperatorSupportsKASCustomKubeconfigLabel = "io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig" + controlPlaneOperatorSupportsGMPMonitoringLabel = "io.openshift.hypershift.control-plane-operator-supports-gmp-monitoring" controlPlaneOperatorAppliesManagementKASNetworkPolicyLabel = "io.openshift.hypershift.control-plane-operator-applies-management-kas-network-policy-label" controlPlanePKIOperatorSignsCSRsLabel = "io.openshift.hypershift.control-plane-pki-operator-signs-csrs" @@ -202,6 +203,10 @@ type HostedClusterReconciler struct { MetricsSet metrics.MetricsSet SREConfigHash string + // EnableGMPMonitoring selects Google Managed Prometheus monitoring resources + // instead of monitoring.coreos.com resources. + EnableGMPMonitoring bool + OperatorNamespace string RegistryProvider globalconfig.RegistryProvider @@ -1564,6 +1569,7 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques }) _, cpoSupportsKASCustomKubeconfig := controlPlaneOperatorImageLabels[controlPlaneOperatorSupportsKASCustomKubeconfigLabel] + enableGMPMonitoring := r.effectiveGMPMonitoring(log, controlPlaneOperatorImageLabels) _, controlPlaneOperatorAppliesManagementKASNetworkPolicyLabel := controlPlaneOperatorImageLabels[controlPlaneOperatorAppliesManagementKASNetworkPolicyLabel] _, controlPlanePKIOperatorSignsCSRs := controlPlaneOperatorImageLabels[controlPlanePKIOperatorSignsCSRsLabel] // Default to true when labels are unavailable (pull secret outage) — all @@ -1644,7 +1650,7 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques report.executeOrBlock("OperatorDeployments", func() error { return r.reconcileOperatorDeployments(ctx, createOrUpdate, hcluster, hcp, controlPlaneNamespace, p, controlPlaneOperatorImage, utilitiesImage, - cpoHasUtilities, r.CertRotationScale, releaseImage, releaseImageVersion, releaseProvider) + cpoHasUtilities, enableGMPMonitoring, r.CertRotationScale, releaseImage, releaseImageVersion, releaseProvider) }) report.executeOrBlock("RBACAndPolicies", func() error { @@ -1812,7 +1818,7 @@ func (r *HostedClusterReconciler) reconcileOperatorDeployments(ctx context.Conte hcluster *hyperv1.HostedCluster, hcp *hyperv1.HostedControlPlane, controlPlaneNamespace *corev1.Namespace, p platform.Platform, controlPlaneOperatorImage, utilitiesImage string, - cpoHasUtilities bool, certRotationScale time.Duration, + cpoHasUtilities, enableGMPMonitoring bool, certRotationScale time.Duration, releaseImage *releaseinfo.ReleaseImage, releaseImageVersion semver.Version, releaseProvider releaseinfo.ProviderWithOpenShiftImageRegistryOverrides, @@ -1842,6 +1848,7 @@ func (r *HostedClusterReconciler) reconcileOperatorDeployments(ctx context.Conte DefaultSecurityContextUID: securityContextUID, EnableCIDebugOutput: r.EnableCIDebugOutput, MetricsSet: metricsSet, + EnableGMPMonitoring: enableGMPMonitoring, ReleaseImageProvider: imageProvider, OmitOwnerReference: true, } @@ -5890,6 +5897,23 @@ func (r *HostedClusterReconciler) effectiveMetricsSet(monitoring hyperv1.Monitor return r.MetricsSet } +// effectiveGMPMonitoring returns whether a hosted control plane should use GMP monitoring. GMP is only +// used when the CPO image declares support, so the components rendered by the hypershift-operator and +// the CPO always emit monitors for the same API group. +func (r *HostedClusterReconciler) effectiveGMPMonitoring(log logr.Logger, controlPlaneOperatorImageLabels map[string]string) bool { + if !r.EnableGMPMonitoring { + return false + } + if _, supported := controlPlaneOperatorImageLabels[controlPlaneOperatorSupportsGMPMonitoringLabel]; !supported { + // Labels are nil when they could not be fetched; operator deployments are blocked in that case. + if controlPlaneOperatorImageLabels != nil { + log.Info("GMP monitoring is enabled but the control plane operator image does not support it, using monitoring.coreos.com resources") + } + return false + } + return true +} + // reconcileSREMetricsConfig loads the SRE metrics configuration (avoids parsing if the content is the same) // and ensures that it is synced to the hosted control plane func (r *HostedClusterReconciler) reconcileSREMetricsConfig(ctx context.Context, createOrUpdate upsert.CreateOrUpdateFN, hcp *hyperv1.HostedControlPlane) error { diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go index 3d39fbc5cd75..69dfd600b81b 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go @@ -84,6 +84,7 @@ import ( "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" "github.com/go-logr/logr" + "github.com/go-logr/logr/funcr" "github.com/google/go-cmp/cmp" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" "go.uber.org/mock/gomock" @@ -9737,3 +9738,49 @@ func TestReconcilePullSecretSync(t *testing.T) { }) } } + +func TestEffectiveGMPMonitoring(t *testing.T) { + supportedLabels := map[string]string{controlPlaneOperatorSupportsGMPMonitoringLabel: "true"} + testCases := []struct { + name string + enableGMPMonitoring bool + labels map[string]string + expected bool + expectLog bool + }{ + { + name: "When GMP monitoring is disabled, it should not use GMP monitoring", + labels: supportedLabels, + }, + { + name: "When GMP monitoring is enabled and the CPO image supports it, it should use GMP monitoring", + enableGMPMonitoring: true, + labels: supportedLabels, + expected: true, + }, + { + name: "When GMP monitoring is enabled and the CPO image does not support it, it should log and not use GMP monitoring", + enableGMPMonitoring: true, + labels: map[string]string{controlPlaneOperatorSubcommandsLabel: "true"}, + expectLog: true, + }, + { + name: "When GMP monitoring is enabled and the CPO image labels are unavailable, it should not use GMP monitoring", + enableGMPMonitoring: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + var logged []string + log := funcr.New(func(_, args string) { logged = append(logged, args) }, funcr.Options{}) + r := &HostedClusterReconciler{EnableGMPMonitoring: tc.enableGMPMonitoring} + g.Expect(r.effectiveGMPMonitoring(log, tc.labels)).To(Equal(tc.expected)) + if tc.expectLog { + g.Expect(logged).To(HaveLen(1)) + } else { + g.Expect(logged).To(BeEmpty()) + } + }) + } +} diff --git a/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go b/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go index 6b7bc9be62ca..6ec9a0092e08 100644 --- a/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go +++ b/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go @@ -1053,6 +1053,7 @@ func (r *HostedClusterReconciler) reconcileLegacy(ctx context.Context, req ctrl. utilitiesImage = r.HypershiftOperatorImage } + enableGMPMonitoring := r.effectiveGMPMonitoring(log, controlPlaneOperatorImageLabels) _, controlPlaneOperatorAppliesManagementKASNetworkPolicyLabel := controlPlaneOperatorImageLabels[controlPlaneOperatorAppliesManagementKASNetworkPolicyLabel] _, controlPlanePKIOperatorSignsCSRs := controlPlaneOperatorImageLabels[controlPlanePKIOperatorSignsCSRsLabel] _, useRestrictedPSA := controlPlaneOperatorImageLabels[useRestrictedPodSecurityLabel] @@ -1700,6 +1701,7 @@ func (r *HostedClusterReconciler) reconcileLegacy(ctx context.Context, req ctrl. DefaultSecurityContextUID: securityContextUID, EnableCIDebugOutput: r.EnableCIDebugOutput, MetricsSet: metricsSet, + EnableGMPMonitoring: enableGMPMonitoring, ReleaseImageProvider: imageProvider, OmitOwnerReference: true, } diff --git a/hypershift-operator/main.go b/hypershift-operator/main.go index 2a6e6e4e9f40..904cf766ff7f 100644 --- a/hypershift-operator/main.go +++ b/hypershift-operator/main.go @@ -66,6 +66,7 @@ import ( "github.com/openshift/hypershift/support/globalconfig" "github.com/openshift/hypershift/support/metrics" "github.com/openshift/hypershift/support/netutil" + "github.com/openshift/hypershift/support/rhobsmonitoring" "github.com/openshift/hypershift/support/supportedversion" "github.com/openshift/hypershift/support/tracing" "github.com/openshift/hypershift/support/upsert" @@ -164,6 +165,7 @@ type StartOptions struct { OIDCStorageProviderS3Credentials string GCPOIDCStorageBucketName string EnableUWMTelemetryRemoteWrite bool + EnableGMPMonitoring bool EnableValidatingWebhook bool EnableDedicatedRequestServingIsolation bool ScaleFromZeroProvider string @@ -209,6 +211,7 @@ func NewStartCommand() *cobra.Command { cmd.Flags().StringVar(&opts.OIDCStorageProviderS3Region, "oidc-storage-provider-s3-region", opts.OIDCStorageProviderS3Region, "Region in which the OIDC bucket is located. Required for AWS guest clusters") cmd.Flags().StringVar(&opts.OIDCStorageProviderS3Credentials, "oidc-storage-provider-s3-credentials", opts.OIDCStorageProviderS3Credentials, "Location of the credentials file for the OIDC bucket. Required for AWS guest clusters.") cmd.Flags().BoolVar(&opts.EnableUWMTelemetryRemoteWrite, "enable-uwm-telemetry-remote-write", opts.EnableUWMTelemetryRemoteWrite, "If true, enables a controller that ensures user workload monitoring is enabled and that it is configured to remote write telemetry metrics from control planes") + cmd.Flags().BoolVar(&opts.EnableGMPMonitoring, "enable-gmp-monitoring", opts.EnableGMPMonitoring, "If true, emit Google Managed Prometheus (monitoring.googleapis.com) monitoring resources instead of monitoring.coreos.com resources") cmd.Flags().BoolVar(&opts.EnableValidatingWebhook, "enable-validating-webhook", false, "Enable webhook for validating hypershift API types") cmd.Flags().BoolVar(&opts.EnableDedicatedRequestServingIsolation, "enable-dedicated-request-serving-isolation", true, "If true, enables scheduling of request serving components to dedicated nodes") cmd.Flags().StringVar(&opts.ScaleFromZeroProvider, "scale-from-zero-provider", opts.ScaleFromZeroProvider, "Platform type for scale-from-zero autoscaling (aws)") @@ -409,6 +412,10 @@ func validateStartOptions(opts *StartOptions, log logr.Logger) error { return fmt.Errorf("--etcd-backup-max-count must be at least 1, got %d", opts.EtcdBackupMaxCount) } + if err := metrics.ValidateGMPMonitoring(opts.EnableGMPMonitoring, os.Getenv(rhobsmonitoring.EnvironmentVariable) == "1"); err != nil { + return fmt.Errorf("--enable-gmp-monitoring: %w", err) + } + supportedProviders := set.New("aws", "azure") if opts.ScaleFromZeroCreds != "" { if opts.ScaleFromZeroProvider == "" { @@ -675,6 +682,7 @@ func setupHostedClusterController(ctx context.Context, mgr ctrl.Manager, opts *S EnableOCPClusterMonitoring: opts.EnableOCPClusterMonitoring, EnableCIDebugOutput: opts.EnableCIDebugOutput, MetricsSet: metricsSet, + EnableGMPMonitoring: opts.EnableGMPMonitoring, OperatorNamespace: opts.Namespace, SREConfigHash: sreConfigHash, KubevirtInfraClients: kvinfra.NewKubevirtInfraClientMap(), diff --git a/hypershift-operator/main_test.go b/hypershift-operator/main_test.go index 244bfda5b24c..ea1075a37ac0 100644 --- a/hypershift-operator/main_test.go +++ b/hypershift-operator/main_test.go @@ -7,8 +7,12 @@ import ( . "github.com/onsi/gomega" + "github.com/openshift/hypershift/support/rhobsmonitoring" + "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/go-logr/logr" ) type fakeHealthCheckManager struct { @@ -69,3 +73,31 @@ func TestSetupHealthChecks(t *testing.T) { }) } } + +func TestValidateStartOptions(t *testing.T) { + testCases := []struct { + name string + enableGMPMonitoring bool + rhobsEnv string + expectError bool + }{ + {name: "When GMP monitoring is disabled, it should pass"}, + {name: "When GMP monitoring is enabled without RHOBS, it should pass", enableGMPMonitoring: true}, + {name: "When RHOBS is enabled without GMP monitoring, it should pass", rhobsEnv: "1"}, + {name: "When GMP monitoring is enabled with RHOBS, it should fail", enableGMPMonitoring: true, rhobsEnv: "1", expectError: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + t.Setenv(rhobsmonitoring.EnvironmentVariable, tc.rhobsEnv) + opts := &StartOptions{EtcdBackupMaxCount: 1, EnableGMPMonitoring: tc.enableGMPMonitoring} + err := validateStartOptions(opts, logr.Discard()) + if tc.expectError { + g.Expect(err).To(MatchError(ContainSubstring("--enable-gmp-monitoring"))) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} diff --git a/renovate.json b/renovate.json index 9bf71bb4a96f..5b6f09693735 100644 --- a/renovate.json +++ b/renovate.json @@ -100,6 +100,16 @@ "matchUpdateTypes": [ "patch" ] + }, + { + "description": "Disable updates (including vulnerability fixes) to the GMP API source pin; bump it deliberately with make gmp-sync, see support/gmpmonitoring/README.md", + "matchManagers": [ + "gomod" + ], + "matchFileNames": [ + "hack/gmp-api-source/go.mod" + ], + "enabled": false } ] } diff --git a/skills/restructure-commits/generate_ownership.py b/skills/restructure-commits/generate_ownership.py index aaf5424afc5e..14ddb7d426f7 100644 --- a/skills/restructure-commits/generate_ownership.py +++ b/skills/restructure-commits/generate_ownership.py @@ -87,6 +87,7 @@ # ---- FACTS ------------------------------------------------------------------------------- GENERATED_GO = re.compile(rb"^// Code generated .* DO NOT EDIT\.$", re.M) +GENERATED_HASH = re.compile(rb"^# Code generated .* DO NOT EDIT\.$", re.M) CRD_MARKER = b"controller-gen.kubebuilder.io/version" MAKE_OUTPUT_DIRS = [ re.compile(r"output:crd:artifacts:config=(\S+)"), @@ -147,6 +148,8 @@ def is_generated(path: str) -> bool: return bool(GENERATED_GO.search(read_head(path, 2048))) if path.endswith((".yaml", ".yml")): return CRD_MARKER in read_head(path) + if path.endswith(".sha256"): + return bool(GENERATED_HASH.search(read_head(path, 2048))) return False diff --git a/skills/restructure-commits/ownership.json b/skills/restructure-commits/ownership.json index 297fde919fc8..ef1a3918f995 100644 --- a/skills/restructure-commits/ownership.json +++ b/skills/restructure-commits/ownership.json @@ -38,6 +38,7 @@ {"pattern": "cmd/install/assets/crds/cluster-api-provider-kubevirt/**", "group": "Vendor", "why": "marked as generated"}, {"pattern": "cmd/install/assets/crds/cluster-api-provider-openstack/**", "group": "Vendor", "why": "marked as generated"}, {"pattern": "cmd/install/assets/crds/external-dns/**", "group": "Vendor", "why": "marked as generated"}, + {"pattern": "support/gmpmonitoring/testdata/**", "group": "Vendor", "why": "marked as generated"}, {"pattern": "cmd/infra/aws/delegating_client.go", "group": "Vendor", "why": "marked as generated"}, {"pattern": "hack/tools/deadcode-report/testdata/reachability/generated.go", "group": "Vendor", "why": "marked as generated"}, {"pattern": "support/awsapi/ec2.go", "group": "Vendor", "why": "marked as generated"}, @@ -47,6 +48,8 @@ {"pattern": "support/awsapi/route53.go", "group": "Vendor", "why": "marked as generated"}, {"pattern": "support/awsapi/s3.go", "group": "Vendor", "why": "marked as generated"}, {"pattern": "support/awsapi/sqs.go", "group": "Vendor", "why": "marked as generated"}, + {"pattern": "support/gmpmonitoring/types.go", "group": "Vendor", "why": "marked as generated"}, + {"pattern": "support/gmpmonitoring/upstream.sha256", "group": "Vendor", "why": "marked as generated"}, {"pattern": "go.mod", "group": "Dependencies", "why": "definition of module root"}, {"pattern": "go.sum", "group": "Dependencies", "why": "definition of module root"}, {"pattern": "api/go.mod", "group": "API", "why": "definition of module api"}, diff --git a/support/api/scheme.go b/support/api/scheme.go index 2dcdf29e17d7..3826fe80fde4 100644 --- a/support/api/scheme.go +++ b/support/api/scheme.go @@ -12,6 +12,7 @@ import ( hyperv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" hyperkarpenterv1 "github.com/openshift/hypershift/api/karpenter/v1" schedulingv1alpha1 "github.com/openshift/hypershift/api/scheduling/v1alpha1" + "github.com/openshift/hypershift/support/gmpmonitoring" "github.com/openshift/hypershift/support/rhobsmonitoring" configv1 "github.com/openshift/api/config/v1" @@ -91,7 +92,7 @@ var ( json.SerializerOptions{Yaml: true, Pretty: true, Strict: false}, ) // AllMonitoringYamlSerializer allows decoding monitoring resources - // from either the default coreos group version or the rhobs group version + // from the CoreOS, RHOBS and GMP group versions AllMonitoringYamlSerializer = json.NewSerializerWithOptions( json.DefaultMetaFactory, AllMonitoringScheme, AllMonitoringScheme, json.SerializerOptions{Yaml: true, Pretty: true, Strict: true}, @@ -146,6 +147,8 @@ func init() { } else { _ = prometheusoperatorv1.AddToScheme(scheme) } + // Type registration is additive; runtime resource selection is separate. + _ = gmpmonitoring.AddToScheme(scheme) _ = snapshotv1.AddToScheme(scheme) _ = mcfgv1.AddToScheme(scheme) _ = cdiv1beta1.AddToScheme(scheme) diff --git a/support/controlplane-component/controlplane-component.go b/support/controlplane-component/controlplane-component.go index 14f61f4a1cb2..a6496ae53044 100644 --- a/support/controlplane-component/controlplane-component.go +++ b/support/controlplane-component/controlplane-component.go @@ -74,6 +74,8 @@ type ControlPlaneContext struct { EnableCIDebugOutput bool // MetricsSet specifies which metrics to use in the service/pod-monitors. MetricsSet metrics.MetricsSet + // EnableGMPMonitoring selects Google Managed Prometheus monitoring resources instead of monitoring.coreos.com resources. + EnableGMPMonitoring bool // OmitOwnerReference determines whether the HCP OwnerReference should be omitted from resources deployed by this component. // This is useful when the component is not managed by the same HostedControlPlane controller like capi and the CPO itself. OmitOwnerReference bool @@ -110,6 +112,7 @@ type WorkloadContext struct { DefaultSecurityContextUID int64 EnableCIDebugOutput bool MetricsSet metrics.MetricsSet + EnableGMPMonitoring bool // skip generation of certificates for unit tests SkipCertificateSigning bool @@ -127,6 +130,7 @@ func (cp *ControlPlaneContext) workloadContext() WorkloadContext { DefaultSecurityContextUID: cp.DefaultSecurityContextUID, EnableCIDebugOutput: cp.EnableCIDebugOutput, MetricsSet: cp.MetricsSet, + EnableGMPMonitoring: cp.EnableGMPMonitoring, ImageMetadataProvider: cp.ImageMetadataProvider, SkipCertificateSigning: cp.SkipCertificateSigning, } diff --git a/support/controlplane-component/controlplane-component_test.go b/support/controlplane-component/controlplane-component_test.go index fd05adfb7506..05dfcc847c0c 100644 --- a/support/controlplane-component/controlplane-component_test.go +++ b/support/controlplane-component/controlplane-component_test.go @@ -387,3 +387,21 @@ func componentsFakeObjects() ([]client.Object, error) { } return fakeObjects, nil } + +func TestWorkloadContext(t *testing.T) { + testCases := []struct { + name string + enableGMPMonitoring bool + }{ + {name: "When GMP monitoring is disabled, it should propagate the disabled value to the workload context"}, + {name: "When GMP monitoring is enabled, it should propagate the enabled value to the workload context", enableGMPMonitoring: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + cp := &ControlPlaneContext{Context: t.Context(), EnableGMPMonitoring: tc.enableGMPMonitoring} + g.Expect(cp.workloadContext().EnableGMPMonitoring).To(Equal(tc.enableGMPMonitoring)) + }) + } +} diff --git a/support/gmpmonitoring/README.md b/support/gmpmonitoring/README.md new file mode 100644 index 000000000000..f2bbb48e0d37 --- /dev/null +++ b/support/gmpmonitoring/README.md @@ -0,0 +1,111 @@ +# GMP API data types + +This package is a maintained API-only extraction from +[`GoogleCloudPlatform/prometheus-engine`](https://github.com/GoogleCloudPlatform/prometheus-engine). +It contains `PodMonitoring`, `Rules`, their lists and the complete closure of +nested data types, including +status, authentication and TLS fields. Upstream license headers, field comments, +JSON tags and generation markers are preserved. The generated `upstream.sha256` +records the resolved source version, upstream commit, module checksums and input +file checksums. + +Collection, rule evaluation, admission, Secret resolution and Prometheus config +construction are excluded. The package imports only Kubernetes API machinery and +the already selected `prometheus/common/model` package. It does not import the +Google operator or require its Prometheus fork. Existing dependency versions stay +unchanged. `ClusterPodMonitoring`, `ClusterRules` and `GlobalRules` are deferred; +management rules spanning HCP namespaces require a separate rule-scope decision. + +`AddToScheme` registers the four supported kinds. HyperShift adds them to both +`Scheme` and `AllMonitoringScheme` regardless of process environment. Existing +CoreOS/RHOBS registration remains unchanged. Registration creates no resources, +clients or watches and does not select a backend at reconciliation time. + +## Maintenance + +The GCP HCP team owns this API snapshot, its extraction tooling and compatibility +validation. Changes to the package and tooling are reviewed by `gcp-reviewers` +through the normal HyperShift review process. + +The source version is selected in +[`hack/gmp-api-source/go.mod`](../../hack/gmp-api-source/go.mod). This is a +download-only module; do not run `go mod tidy` there. HyperShift does not import +the upstream operator package. Do not edit generated types, deepcopies or +checksums by hand. + +Version bumps are deliberate. The pin should track the GMP version deployed by GKE, +not the latest upstream release, so `renovate.json` disables automated dependency +updates for `hack/gmp-api-source/go.mod`. The regeneration tooling does not monitor +upstream releases or establish compatibility with deployed GKE/GMP components. + +### Regenerate the current snapshot + +From the repository root, with Go, Python 3 and Make available: + +```sh +make gmp-sync +make verify-gmp-sync +``` + +Regenerating an unchanged source pin should produce no differences. The updater +downloads the selected module in isolation, extracts the supported data types, +copies the upstream license and CRD fixtures, records provenance and generates +deepcopies with the repository's pinned `controller-gen`. It does not change +HyperShift's root module or vendor directory. Ordinary `make generate` continues +to regenerate deepcopies offline. + +`make verify-gmp-sync` verifies the recorded inputs and regenerates into temporary +storage. It fails on missing, different or unexpected generated files without +rewriting tracked files. It runs as part of `make lint` (and therefore `make +verify-ci`, `make verify`, and `make verify-parallel`), so a stale snapshot fails +CI; run it manually after any GMP snapshot, tooling or dependency change to catch +drift before pushing. `make gmp-sync` runs as part of `make update`. An offline +unit test in `hack/gmp-api-gen` runs with the regular unit tests and fails if +`upstream.sha256` no longer matches the version and checksums pinned in +`hack/gmp-api-source`. + +### Update to a newer upstream version + +1. Select a source version appropriate for the supported GKE/GMP environments and + review its relevant API/schema changes with the GCP HCP team. +2. Update the `prometheus-engine` requirement in `hack/gmp-api-source/go.mod`. +3. Run `make gmp-sync`. +4. Run `make verify-gmp-sync` and the focused tests: + + ```sh + GO111MODULE=on GOWORK=off GOFLAGS=-mod=vendor go test -race \ + ./hack/gmp-api-gen ./support/gmpmonitoring + ``` + +5. Review the generated diff and include compatibility evidence in the PR. + +Expected changes are limited to the source pin/checksums, generated provenance, +API types, deepcopies, license and schema fixtures. Unexpected runtime dependencies +or extraction failures require review by the owning team; do not work around them +by editing generated files. The extractor follows the type closure plus the +monitoring condition constant group only. If an added type requires another +constant, extend the generator deliberately. + +The pinned source is always downloaded with checksum database verification +(`GOSUMDB=sum.golang.org`), regardless of local `GOPRIVATE`/`GONOSUMDB` settings. +Generation fails if copied upstream source contains Go directives such as +`//go:generate` or build constraints. + +## Compatibility checks + +The copied source version does not need to numerically match the deployed GMP +image version. Compatibility depends on the fields HyperShift emits and the +schemas and behavior supported by the deployed components. Record the GKE version, +GMP operator image and validation performed when assessing compatibility. + +Tests cover scheme registration in fresh default (CoreOS) and RHOBS processes, actual +HyperShift YAML/JSON serializer round trips (objects and lists, including status), +deepcopy isolation and emitted objects against pinned upstream v1 OpenAPI schemas +with negative controls. Schema fixtures are test input only; HyperShift does not +install these CRDs. + +Offline schema validation checks OpenAPI structure and constraints. It does not +execute Kubernetes CEL validation or Google's admission/runtime checks. The pinned +source is a serialization reference, not a claim that every deployed GKE cluster +uses that schema. Live CRD compatibility, scrape behavior and rule evaluation require +later validation against the selected GKE/GMP versions. diff --git a/support/gmpmonitoring/doc.go b/support/gmpmonitoring/doc.go new file mode 100644 index 000000000000..cca642030f63 --- /dev/null +++ b/support/gmpmonitoring/doc.go @@ -0,0 +1,8 @@ +// Package gmpmonitoring contains upstream-derived monitoring.googleapis.com/v1 +// API data types only. Collection, admission and rule evaluation belong to GMP. +// +// +kubebuilder:object:generate=true +// +groupName=monitoring.googleapis.com +package gmpmonitoring + +//go:generate ../../hack/tools/bin/controller-gen object:headerFile=../../hack/boilerplate.go.txt paths=. diff --git a/support/gmpmonitoring/scheme.go b/support/gmpmonitoring/scheme.go new file mode 100644 index 000000000000..ff41b19c8f63 --- /dev/null +++ b/support/gmpmonitoring/scheme.go @@ -0,0 +1,24 @@ +package gmpmonitoring + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// SchemeGroupVersion is the API version of the copied GMP types. +var SchemeGroupVersion = schema.GroupVersion{Group: "monitoring.googleapis.com", Version: "v1"} + +// SchemeBuilder registers only the supported monitoring data objects. +var SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + +// AddToScheme registers GMP types without installing CRDs or starting watches. +var AddToScheme = SchemeBuilder.AddToScheme + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &PodMonitoring{}, &PodMonitoringList{}, &Rules{}, &RulesList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/support/gmpmonitoring/testdata/podmonitorings.crd.yaml b/support/gmpmonitoring/testdata/podmonitorings.crd.yaml new file mode 100644 index 000000000000..647441e075f6 --- /dev/null +++ b/support/gmpmonitoring/testdata/podmonitorings.crd.yaml @@ -0,0 +1,1131 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1-0.20250103184936-50893dee96da + name: podmonitorings.monitoring.googleapis.com +spec: + group: monitoring.googleapis.com + names: + kind: PodMonitoring + listKind: PodMonitoringList + plural: podmonitorings + singular: podmonitoring + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: |- + PodMonitoring defines monitoring for a set of pods, scoped to pods + within the PodMonitoring's namespace. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + Specification of desired Pod selection for target discovery by + Prometheus. + properties: + endpoints: + description: The endpoints to scrape on the selected pods. + items: + description: ScrapeEndpoint specifies a Prometheus metrics endpoint + to scrape. + properties: + authorization: + description: Authorization is the HTTP authorization credentials + for the targets. + properties: + credentials: + description: Credentials uses the secret as the credentials + (token) for the authentication header. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + type: + description: |- + Type is the authentication type. Defaults to Bearer. + Basic will cause an error, as the BasicAuth object should be used instead. + type: string + x-kubernetes-validations: + - message: authorization type cannot be set to "basic", + use "basic_auth" instead + rule: self != 'Basic' + type: object + basicAuth: + description: BasicAuth is the HTTP basic authentication credentials + for the targets. + properties: + password: + description: Password uses the secret as the BasicAuth password. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + username: + description: Username is the BasicAuth username. + type: string + type: object + interval: + description: Interval at which to scrape metrics. Must be a + valid Prometheus duration. + format: duration + type: string + metricRelabeling: + description: |- + Relabeling rules for metrics scraped from this endpoint. Relabeling rules that + override protected target labels (project_id, location, cluster, namespace, job, + instance, top_level_controller, top_level_controller_type, or __address__) are + not permitted. The labelmap action is not permitted in general. + items: + description: RelabelingRule defines a single Prometheus relabeling + rule. + properties: + action: + description: Action to perform based on regex matching. + Defaults to 'replace'. + enum: + - replace + - lowercase + - uppercase + - keep + - drop + - keepequal + - dropequal + - hashmod + - labeldrop + - labelkeep + type: string + modulus: + description: Modulus to take of the hash of the source + label values. + format: int64 + type: integer + regex: + description: Regular expression against which the extracted + value is matched. Defaults to '(.*)'. + maxLength: 10000 + type: string + replacement: + description: |- + Replacement value against which a regex replace is performed if the + regular expression matches. Regex capture groups are available. Defaults to '$1'. + type: string + separator: + description: Separator placed between concatenated source + label values. Defaults to ';'. + type: string + sourceLabels: + description: |- + The source labels select values from existing labels. Their content is concatenated + using the configured separator and matched against the configured regular expression + for the replace, keep, and drop actions. + items: + pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$ + type: string + maxItems: 100 + type: array + targetLabel: + description: |- + Label to which the resulting value is written in a replace action. + It is mandatory for replace actions. Regex capture groups are available. + pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$ + type: string + x-kubernetes-validations: + - messageExpression: '''cannot relabel onto protected + label "%s"''.format([self])' + rule: self != 'project_id' && self != 'location' && + self != 'cluster' && self != 'namespace' && self != + 'job' && self != 'instance' && self != 'top_level_controller' + && self != 'top_level_controller_type' && self != + '__address__' + type: object + x-kubernetes-validations: + - rule: '!has(self.action) || self.action != ''labeldrop'' + || has(self.regex)' + maxItems: 250 + type: array + oauth2: + description: OAuth2 is the OAuth2 client credentials used to + fetch a token for the targets. + properties: + clientID: + description: ClientID is the public identifier for the client. + type: string + clientSecret: + description: ClientSecret uses the secret as the client + secret token. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + endpointParams: + additionalProperties: + type: string + description: EndpointParams are additional parameters to + append to the token URL. + type: object + proxyUrl: + description: |- + ProxyURL is the HTTP proxy server to use to connect to the targets. + + Encoded passwords are not supported. + maxLength: 2000 + type: string + x-kubernetes-validations: + - rule: isURL(self) && !self.matches('@') + scopes: + description: Scopes represents the scopes for the token + request. + items: + type: string + type: array + tlsConfig: + description: TLS configures the token request's TLS settings. + properties: + ca: + description: |- + SecretSelector references a secret from a secret provider e.g. Kubernetes Secret. Only one + provider can be used at a time. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. + Must be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + cert: + description: Cert uses the secret as the certificate + for client authentication to the server. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. + Must be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + insecureSkipVerify: + description: InsecureSkipVerify disables target certificate + validation. + type: boolean + key: + description: Key uses the secret as the private key + for client authentication to the server. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. + Must be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + maxVersion: + description: |- + MaxVersion is the maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), + TLS12 (TLS 1.2), TLS13 (TLS 1.3). + + If unset, Prometheus will use Go default minimum version, which is TLS 1.2. + See MinVersion in https://pkg.go.dev/crypto/tls#Config. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + minVersion: + description: |- + MinVersion is the minimum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), + TLS12 (TLS 1.2), TLS13 (TLS 1.3). + + If unset, Prometheus will use Go default minimum version, which is TLS 1.2. + See MinVersion in https://pkg.go.dev/crypto/tls#Config. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + serverName: + description: ServerName is used to verify the hostname + for the targets. + type: string + type: object + x-kubernetes-validations: + - message: client cert and client key must be provided together, + when either is provided + rule: has(self.cert) == has(self.key) + tokenURL: + description: TokenURL is the URL to fetch the token from. + type: string + type: object + params: + additionalProperties: + items: + type: string + type: array + description: HTTP GET params to use when scraping. + type: object + path: + description: HTTP path to scrape metrics from. Defaults to "/metrics". + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to scrape. + The container metadata label is only populated if the port is referenced by name + because port numbers are not unique across containers. + maxLength: 253 + minLength: 1 + x-kubernetes-int-or-string: true + x-kubernetes-validations: + - message: Port is required + rule: self != 0 + proxyUrl: + description: |- + ProxyURL is the HTTP proxy server to use to connect to the targets. + + Encoded passwords are not supported. + maxLength: 2000 + type: string + x-kubernetes-validations: + - rule: isURL(self) && !self.matches('@') + scheme: + description: Protocol scheme to use to scrape. + enum: + - http + - https + type: string + timeout: + description: |- + Timeout for metrics scrapes. Must be a valid Prometheus duration. + Must not be larger than the scrape interval. + format: duration + type: string + tls: + description: TLS configures the scrape request's TLS settings. + properties: + ca: + description: |- + SecretSelector references a secret from a secret provider e.g. Kubernetes Secret. Only one + provider can be used at a time. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + cert: + description: Cert uses the secret as the certificate for + client authentication to the server. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + insecureSkipVerify: + description: InsecureSkipVerify disables target certificate + validation. + type: boolean + key: + description: Key uses the secret as the private key for + client authentication to the server. + properties: + secret: + description: Secret represents reference to a given + key from certain Secret in a given namespace. + properties: + key: + description: Key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: Name of the secret to select from. + type: string + namespace: + description: |- + Namespace of the secret to select from. + If empty the parent resource namespace will be chosen. + type: string + required: + - key + - name + type: object + type: object + maxVersion: + description: |- + MaxVersion is the maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), + TLS12 (TLS 1.2), TLS13 (TLS 1.3). + + If unset, Prometheus will use Go default minimum version, which is TLS 1.2. + See MinVersion in https://pkg.go.dev/crypto/tls#Config. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + minVersion: + description: |- + MinVersion is the minimum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), + TLS12 (TLS 1.2), TLS13 (TLS 1.3). + + If unset, Prometheus will use Go default minimum version, which is TLS 1.2. + See MinVersion in https://pkg.go.dev/crypto/tls#Config. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + serverName: + description: ServerName is used to verify the hostname for + the targets. + type: string + type: object + x-kubernetes-validations: + - message: client cert and client key must be provided together, + when either is provided + rule: has(self.cert) == has(self.key) + required: + - interval + - port + type: object + x-kubernetes-validations: + - messageExpression: '''scrape timeout (%s) must not be greater + than scrape interval (%s)''.format([self.timeout, self.interval])' + rule: '!has(self.timeout) || self.timeout <= self.interval' + - rule: '((has(self.authorization) ? 1 : 0) + (has(self.basicAuth) + ? 1 : 0) + (has(self.oauth2) ? 1 : 0)) <= 1' + maxItems: 10 + minItems: 1 + type: array + filterRunning: + description: |- + FilterRunning will drop any pods that are in the "Failed" or "Succeeded" + pod lifecycle. + See: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + type: boolean + limits: + description: Limits to apply at scrape time. + properties: + labelNameLength: + description: |- + Maximum label name length. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + labelValueLength: + description: |- + Maximum label value length. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + labels: + description: |- + Maximum number of labels accepted for a single sample. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + samples: + description: |- + Maximum number of samples accepted within a single scrape. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + type: object + selector: + description: |- + Label selector that specifies which pods are selected for this monitoring + configuration. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + targetLabels: + description: |- + Labels to add to the Prometheus target for discovered endpoints. + The `instance` label is always set to `:` or `:` + if the scraped pod is controlled by a DaemonSet. + properties: + fromPod: + description: |- + Labels to transfer from the Kubernetes Pod to Prometheus target labels. + Mappings are applied in order. + items: + description: |- + LabelMapping specifies how to transfer a label from a Kubernetes resource + onto a Prometheus target. + properties: + from: + description: Kubernetes resource label to remap. + type: string + to: + description: |- + Remapped Prometheus target label. + Defaults to the same name as `From`. + pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$ + type: string + x-kubernetes-validations: + - messageExpression: '''cannot relabel onto protected label + "%s"''.format([self])' + rule: self != 'project_id' && self != 'location' && self + != 'cluster' && self != 'namespace' && self != 'job' + && self != 'instance' && self != 'top_level_controller' + && self != 'top_level_controller_type' && self != '__address__' + required: + - from + type: object + maxItems: 100 + type: array + metadata: + default: + - container + - pod + - top_level_controller_name + - top_level_controller_type + description: |- + Pod metadata labels that are set on all scraped targets. + Permitted keys are `container`, `node`, `pod`, `top_level_controller_name`, + and `top_level_controller_type`. The `container` + label is only populated if the scrape port is referenced by name. + Defaults to [container, pod, top_level_controller_name, top_level_controller_type]. + If set to null, it will be interpreted as the empty list. + This is for backwards-compatibility only. + items: + enum: + - container + - node + - pod + - top_level_controller_name + - top_level_controller_type + type: string + type: array + x-kubernetes-list-type: set + type: object + required: + - endpoints + - selector + type: object + status: + description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's + current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + endpointStatuses: + description: Represents the latest available observations of target + state for each ScrapeEndpoint. + items: + properties: + activeTargets: + description: Total number of active targets. + format: int64 + type: integer + collectorsFraction: + description: |- + Fraction of collectors included in status, bounded [0,1]. + Ideally, this should always be 1. Anything less can + be considered a problem and should be investigated. + type: string + lastUpdateTime: + description: Last time this status was updated. + format: date-time + type: string + name: + description: The name of the ScrapeEndpoint. + type: string + sampleGroups: + description: A fixed sample of targets grouped by error type. + items: + properties: + count: + description: Total count of similar errors. + format: int32 + type: integer + sampleTargets: + description: Targets emitting the error message. + items: + properties: + health: + description: Health status. + type: string + labels: + additionalProperties: + description: A LabelValue is an associated value + for a LabelName. + type: string + description: The label set, keys and values, of + the target. + type: object + lastError: + description: Error message. + type: string + lastScrapeDurationSeconds: + description: Scrape duration in seconds. + type: string + type: object + type: array + type: object + type: array + unhealthyTargets: + description: Total number of active, unhealthy targets. + format: int64 + type: integer + required: + - name + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: Namespace not allowed on PodMonitoring secret references. + reason: FieldValueForbidden + rule: self.spec.endpoints.all(e, !has(e.authorization) || !has(e.authorization.credentials) + || !has(e.authorization.credentials.secret) || !has(e.authorization.credentials.secret.__namespace__)) + - message: Namespace not allowed on PodMonitoring secret references. + reason: FieldValueForbidden + rule: self.spec.endpoints.all(e, !has(e.basicAuth) || !has(e.basicAuth.password) + || !has(e.basicAuth.password.secret) || !has(e.basicAuth.password.secret.__namespace__)) + - message: Namespace not allowed on PodMonitoring secret references. + reason: FieldValueForbidden + rule: self.spec.endpoints.all(e, !has(e.tls) || !has(e.tls.ca) || !has(e.tls.ca.secret) + || !has(e.tls.ca.secret.__namespace__)) + - message: Namespace not allowed on PodMonitoring secret references. + reason: FieldValueForbidden + rule: self.spec.endpoints.all(e, !has(e.tls) || !has(e.tls.cert) || !has(e.tls.cert.secret) + || !has(e.tls.cert.secret.__namespace__)) + - message: Namespace not allowed on PodMonitoring secret references. + reason: FieldValueForbidden + rule: self.spec.endpoints.all(e, !has(e.tls) || !has(e.tls.key) || !has(e.tls.key.secret) + || !has(e.tls.key.secret.__namespace__)) + - message: Namespace not allowed on PodMonitoring secret references. + reason: FieldValueForbidden + rule: self.spec.endpoints.all(e, !has(e.oauth2) || !has(e.oauth2.clientSecret) + || !has(e.oauth2.clientSecret.secret) || !has(e.oauth2.clientSecret.secret.__namespace__)) + served: true + storage: true + subresources: + status: {} + - deprecated: true + name: v1alpha1 + schema: + openAPIV3Schema: + description: PodMonitoring defines monitoring for a set of pods. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + Specification of desired Pod selection for target discovery by + Prometheus. + properties: + endpoints: + description: The endpoints to scrape on the selected pods. + items: + description: ScrapeEndpoint specifies a Prometheus metrics endpoint + to scrape. + properties: + interval: + description: Interval at which to scrape metrics. Must be a + valid Prometheus duration. + type: string + metricRelabeling: + description: |- + Relabeling rules for metrics scraped from this endpoint. Relabeling rules that + override protected target labels (project_id, location, cluster, namespace, job, + instance, or __address__) are not permitted. The labelmap action is not permitted + in general. + items: + description: RelabelingRule defines a single Prometheus relabeling + rule. + properties: + action: + description: Action to perform based on regex matching. + Defaults to 'replace'. + type: string + modulus: + description: Modulus to take of the hash of the source + label values. + format: int64 + type: integer + regex: + description: Regular expression against which the extracted + value is matched. Defaults to '(.*)'. + type: string + replacement: + description: |- + Replacement value against which a regex replace is performed if the + regular expression matches. Regex capture groups are available. Defaults to '$1'. + type: string + separator: + description: Separator placed between concatenated source + label values. Defaults to ';'. + type: string + sourceLabels: + description: |- + The source labels select values from existing labels. Their content is concatenated + using the configured separator and matched against the configured regular expression + for the replace, keep, and drop actions. + items: + type: string + type: array + targetLabel: + description: |- + Label to which the resulting value is written in a replace action. + It is mandatory for replace actions. Regex capture groups are available. + type: string + type: object + type: array + params: + additionalProperties: + items: + type: string + type: array + description: HTTP GET params to use when scraping. + type: object + path: + description: HTTP path to scrape metrics from. Defaults to "/metrics". + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to scrape. + x-kubernetes-int-or-string: true + proxyUrl: + description: Proxy URL to scrape through. Encoded passwords + are not supported. + type: string + scheme: + description: Protocol scheme to use to scrape. + type: string + timeout: + description: |- + Timeout for metrics scrapes. Must be a valid Prometheus duration. + Must not be larger then the scrape interval. + type: string + required: + - port + type: object + type: array + limits: + description: Limits to apply at scrape time. + properties: + labelNameLength: + description: |- + Maximum label name length. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + labelValueLength: + description: |- + Maximum label value length. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + labels: + description: |- + Maximum number of labels accepted for a single sample. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + samples: + description: |- + Maximum number of samples accepted within a single scrape. + Uses Prometheus default if left unspecified. + format: int64 + type: integer + type: object + selector: + description: |- + Label selector that specifies which pods are selected for this monitoring + configuration. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + targetLabels: + description: Labels to add to the Prometheus target for discovered + endpoints. + properties: + fromPod: + description: |- + Labels to transfer from the Kubernetes Pod to Prometheus target labels. + Mappings are applied in order. + items: + description: |- + LabelMapping specifies how to transfer a label from a Kubernetes resource + onto a Prometheus target. + properties: + from: + description: Kubenetes resource label to remap. + type: string + to: + description: |- + Remapped Prometheus target label. + Defaults to the same name as `From`. + type: string + required: + - from + type: object + type: array + metadata: + description: |- + Pod metadata labels that are set on all scraped targets. + Permitted keys are `pod`, `container`, and `node` for PodMonitoring and + `pod`, `container`, `node`, and `namespace` for ClusterPodMonitoring. + Defaults to [pod, container] for PodMonitoring and [namespace, pod, container] + for ClusterPodMonitoring. + If set to null, it will be interpreted as the empty list for PodMonitoring + and to [namespace] for ClusterPodMonitoring. This is for backwards-compatibility + only. + items: + type: string + type: array + type: object + required: + - endpoints + - selector + type: object + status: + description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's + current state. + items: + description: MonitoringCondition describes a condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} diff --git a/support/gmpmonitoring/testdata/rules.crd.yaml b/support/gmpmonitoring/testdata/rules.crd.yaml new file mode 100644 index 000000000000..f17697ed140c --- /dev/null +++ b/support/gmpmonitoring/testdata/rules.crd.yaml @@ -0,0 +1,289 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1-0.20250103184936-50893dee96da + name: rules.monitoring.googleapis.com +spec: + group: monitoring.googleapis.com + names: + kind: Rules + listKind: RulesList + plural: rules + singular: rules + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: |- + Rules defines Prometheus alerting and recording rules that are scoped + to the namespace of the resource. Only metric data from this namespace is processed + and all rule results have their project_id, cluster, and namespace label preserved + for query processing. + If the location label is not preserved by the rule, it defaults to the cluster's location. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Specification of rules to record and alert on. + properties: + groups: + description: A list of Prometheus rule groups. + items: + description: |- + RuleGroup declares rules in the Prometheus format: + https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ + properties: + interval: + default: 1m + description: The interval at which to evaluate the rules. Must + be a valid Prometheus duration. + format: duration + type: string + name: + description: The name of the rule group. + type: string + rules: + description: A list of rules that are executed sequentially + as part of this group. + items: + description: |- + Rule is a single rule in the Prometheus format: + https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ + properties: + alert: + description: |- + Name of the alert to evaluate the expression as. + Only one of `record` and `alert` must be set. + type: string + annotations: + additionalProperties: + type: string + description: |- + A set of annotations to attach to alerts produced by the query expression. + Only valid if `alert` is set. + type: object + expr: + description: The PromQL expression to evaluate. + type: string + for: + description: |- + The duration to wait before a firing alert produced by this rule is sent to Alertmanager. + Only valid if `alert` is set. + format: duration + type: string + labels: + additionalProperties: + type: string + description: A set of labels to attach to the result of + the query expression. + type: object + record: + description: |- + Record the result of the expression to this metric name. + Only one of `record` and `alert` must be set. + pattern: ^[a-zA-Z_:][a-zA-Z0-9_:]*$ + type: string + required: + - expr + type: object + x-kubernetes-validations: + - message: Must set exactly one of Record or Alert + rule: '(has(self.record) ? 1 : 0) + (has(self.alert) ? 1 + : 0) == 1' + - message: Annotations are only allowed for alerting rules + rule: '!has(self.annotations) || has(self.alert)' + minItems: 1 + type: array + required: + - name + - rules + type: object + type: array + required: + - groups + type: object + status: + description: Most recently observed status of the resource. + properties: + conditions: + description: Represents the latest available observations of a podmonitor's + current state. + items: + description: MonitoringCondition describes the condition of a PodMonitoring. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: MonitoringConditionType is the type of MonitoringCondition. + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - deprecated: true + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + Rules defines Prometheus alerting and recording rules that are scoped + to the namespace of the resource. Only metric data from this namespace is processed + and all rule results have their project_id, cluster, and namespace label preserved + for query processing. + If the location label is not preserved by the rule, it defaults to the cluster's location. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Specification of rules to record and alert on. + properties: + groups: + description: A list of Prometheus rule groups. + items: + description: |- + RuleGroup declares rules in the Prometheus format: + https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ + properties: + interval: + description: The interval at which to evaluate the rules. Must + be a valid Prometheus duration. + type: string + name: + description: The name of the rule group. + type: string + rules: + description: A list of rules that are executed sequentially + as part of this group. + items: + description: |- + Rule is a single rule in the Prometheus format: + https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ + properties: + alert: + description: |- + Name of the alert to evaluate the expression as. + Only one of `record` and `alert` must be set. + type: string + annotations: + additionalProperties: + type: string + description: |- + A set of annotations to attach to alerts produced by the query expression. + Only valid if `alert` is set. + type: object + expr: + description: The PromQL expression to evaluate. + type: string + for: + description: |- + The duration to wait before a firing alert produced by this rule is sent to Alertmanager. + Only valid if `alert` is set. + type: string + labels: + additionalProperties: + type: string + description: A set of labels to attach to the result of + the query expression. + type: object + record: + description: |- + Record the result of the expression to this metric name. + Only one of `record` and `alert` must be set. + type: string + required: + - expr + type: object + type: array + required: + - interval + - name + - rules + type: object + type: array + required: + - groups + type: object + status: + description: Most recently observed status of the resource. + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} diff --git a/support/gmpmonitoring/types.go b/support/gmpmonitoring/types.go new file mode 100644 index 000000000000..7ac65931bc04 --- /dev/null +++ b/support/gmpmonitoring/types.go @@ -0,0 +1,487 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by hack/update-gmp-api.sh; DO NOT EDIT. + +package gmpmonitoring + +import ( + prommodel "github.com/prometheus/common/model" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + intstr "k8s.io/apimachinery/pkg/util/intstr" +) + +const ( + // ConfigurationCreateSuccess indicates that the config generated from the + // monitoring resource was created successfully. + ConfigurationCreateSuccess MonitoringConditionType = "ConfigurationCreateSuccess" +) + +// Auth sets the `Authorization` header on every HTTP request. +type Auth struct { + // Type is the authentication type. Defaults to Bearer. + // Basic will cause an error, as the BasicAuth object should be used instead. + // +kubebuilder:validation:XValidation:rule="self != 'Basic'",message="authorization type cannot be set to \"basic\", use \"basic_auth\" instead" + // +optional + Type string `json:"type,omitempty"` + // Credentials uses the secret as the credentials (token) for the authentication header. + // +optional + Credentials *SecretSelector `json:"credentials,omitempty"` +} + +// BasicAuth sets the `Authorization` header on every HTTP request with the configured username +// and password. +type BasicAuth struct { + // Username is the BasicAuth username. + // +optional + Username string `json:"username,omitempty"` + // Password uses the secret as the BasicAuth password. + // +optional + Password *SecretSelector `json:"password,omitempty"` +} + +// HTTPClientConfig stores HTTP-client configurations. +// +kubebuilder:validation:XValidation:rule="((has(self.authorization) ? 1 : 0) + (has(self.basicAuth) ? 1 : 0) + (has(self.oauth2) ? 1 : 0)) <= 1" +type HTTPClientConfig struct { + // Authorization is the HTTP authorization credentials for the targets. + // +optional + Authorization *Auth `json:"authorization,omitempty"` + // BasicAuth is the HTTP basic authentication credentials for the targets. + // +optional + BasicAuth *BasicAuth `json:"basicAuth,omitempty"` + // TLS configures the scrape request's TLS settings. + // +optional + TLS *TLS `json:"tls,omitempty"` + // OAuth2 is the OAuth2 client credentials used to fetch a token for the targets. + // +optional + OAuth2 *OAuth2 `json:"oauth2,omitempty"` + ProxyConfig `json:",inline"` +} + +// LabelMapping specifies how to transfer a label from a Kubernetes resource +// onto a Prometheus target. +type LabelMapping struct { + // Kubernetes resource label to remap. + From string `json:"from"` + // Remapped Prometheus target label. + // Defaults to the same name as `From`. + // +kubebuilder:validation:Pattern=^[a-zA-Z_][a-zA-Z0-9_]*$ + // +kubebuilder:validation:MaxLength:100 + // +kubebuilder:validation:XValidation:rule="self != 'project_id' && self != 'location' && self != 'cluster' && self != 'namespace' && self != 'job' && self != 'instance' && self != 'top_level_controller' && self != 'top_level_controller_type' && self != '__address__'",messageExpression="'cannot relabel onto protected label \"%s\"'.format([self])" + To string `json:"to,omitempty"` +} + +// MonitoringCondition describes the condition of a PodMonitoring. +type MonitoringCondition struct { + Type MonitoringConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status"` + // The last time this condition was updated. + // +optional + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` + // Last time the condition transitioned from one status to another. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + // The reason for the condition's last transition. + // +optional + Reason string `json:"reason,omitempty"` + // A human-readable message indicating details about the transition. + // +optional + Message string `json:"message,omitempty"` +} + +// MonitoringConditionType is the type of MonitoringCondition. +type MonitoringConditionType string + +// MonitoringStatus holds status information of a monitoring resource. +type MonitoringStatus struct { + // The generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration"` + // Represents the latest available observations of a podmonitor's current state. + Conditions []MonitoringCondition `json:"conditions,omitempty"` +} + +// OAuth2 is the OAuth2 client configuration. +type OAuth2 struct { + // ClientID is the public identifier for the client. + // +optional + ClientID string `json:"clientID"` + // ClientSecret uses the secret as the client secret token. + // +optional + ClientSecret *SecretSelector `json:"clientSecret,omitempty"` + // Scopes represents the scopes for the token request. + // +optional + Scopes []string `json:"scopes,omitempty"` + // TokenURL is the URL to fetch the token from. + // +optional + TokenURL string `json:"tokenURL"` + // EndpointParams are additional parameters to append to the token URL. + // +optional + EndpointParams map[string]string `json:"endpointParams,omitempty"` + // TLS configures the token request's TLS settings. + // +optional + TLS *TLS `json:"tlsConfig,omitempty"` + ProxyConfig `json:",inline"` +} + +// PodMonitoring defines monitoring for a set of pods, scoped to pods +// within the PodMonitoring's namespace. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:validation:XValidation:rule="self.spec.endpoints.all(e, !has(e.authorization) || !has(e.authorization.credentials) || !has(e.authorization.credentials.secret) || !has(e.authorization.credentials.secret.__namespace__))",message="Namespace not allowed on PodMonitoring secret references.",reason="FieldValueForbidden" +// +kubebuilder:validation:XValidation:rule="self.spec.endpoints.all(e, !has(e.basicAuth) || !has(e.basicAuth.password) || !has(e.basicAuth.password.secret) || !has(e.basicAuth.password.secret.__namespace__))",message="Namespace not allowed on PodMonitoring secret references.",reason="FieldValueForbidden" +// +kubebuilder:validation:XValidation:rule="self.spec.endpoints.all(e, !has(e.tls) || !has(e.tls.ca) || !has(e.tls.ca.secret) || !has(e.tls.ca.secret.__namespace__))",message="Namespace not allowed on PodMonitoring secret references.",reason="FieldValueForbidden" +// +kubebuilder:validation:XValidation:rule="self.spec.endpoints.all(e, !has(e.tls) || !has(e.tls.cert) || !has(e.tls.cert.secret) || !has(e.tls.cert.secret.__namespace__))",message="Namespace not allowed on PodMonitoring secret references.",reason="FieldValueForbidden" +// +kubebuilder:validation:XValidation:rule="self.spec.endpoints.all(e, !has(e.tls) || !has(e.tls.key) || !has(e.tls.key.secret) || !has(e.tls.key.secret.__namespace__))",message="Namespace not allowed on PodMonitoring secret references.",reason="FieldValueForbidden" +// +kubebuilder:validation:XValidation:rule="self.spec.endpoints.all(e, !has(e.oauth2) || !has(e.oauth2.clientSecret) || !has(e.oauth2.clientSecret.secret) || !has(e.oauth2.clientSecret.secret.__namespace__))",message="Namespace not allowed on PodMonitoring secret references.",reason="FieldValueForbidden" +type PodMonitoring struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of desired Pod selection for target discovery by + // Prometheus. + Spec PodMonitoringSpec `json:"spec"` + // Most recently observed status of the resource. + // +optional + Status PodMonitoringStatus `json:"status"` +} + +// PodMonitoringList is a list of PodMonitorings. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type PodMonitoringList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PodMonitoring `json:"items"` +} + +// PodMonitoringSpec contains specification parameters for PodMonitoring. +type PodMonitoringSpec struct { + // Label selector that specifies which pods are selected for this monitoring + // configuration. + Selector metav1.LabelSelector `json:"selector"` + // The endpoints to scrape on the selected pods. + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=10 + Endpoints []ScrapeEndpoint `json:"endpoints"` + // Labels to add to the Prometheus target for discovered endpoints. + // The `instance` label is always set to `:` or `:` + // if the scraped pod is controlled by a DaemonSet. + TargetLabels TargetLabels `json:"targetLabels,omitempty"` + // Limits to apply at scrape time. + Limits *ScrapeLimits `json:"limits,omitempty"` + // FilterRunning will drop any pods that are in the "Failed" or "Succeeded" + // pod lifecycle. + // See: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + FilterRunning *bool `json:"filterRunning,omitempty"` +} + +// PodMonitoringStatus holds status information of a PodMonitoring resource. +type PodMonitoringStatus struct { + MonitoringStatus `json:",inline"` + // Represents the latest available observations of target state for each ScrapeEndpoint. + EndpointStatuses []ScrapeEndpointStatus `json:"endpointStatuses,omitempty"` +} + +// ProxyConfig specifies the proxy HTTP configuration. +type ProxyConfig struct { + // ProxyURL is the HTTP proxy server to use to connect to the targets. + // + // Encoded passwords are not supported. + // +optional + // +kubebuilder:validation:MaxLength=2000 + // +kubebuilder:validation:XValidation:rule="isURL(self) && !self.matches('@')" + ProxyURL string `json:"proxyUrl,omitempty"` + + // TODO(TheSpiritXIII): Consider adding further fields for Proxy configuration, similar to https://prometheus.io/docs/prometheus/latest/configuration/configuration/#oauth2 +} + +// RelabelingRule defines a single Prometheus relabeling rule. +// +kubebuilder:validation:XValidation:rule="!has(self.action) || self.action != 'labeldrop' || has(self.regex)" +type RelabelingRule struct { + // The source labels select values from existing labels. Their content is concatenated + // using the configured separator and matched against the configured regular expression + // for the replace, keep, and drop actions. + // +kubebuilder:validation:MaxItems=100 + // +kubebuilder:validation:items:Pattern=^[a-zA-Z_][a-zA-Z0-9_]*$ + SourceLabels []string `json:"sourceLabels,omitempty"` + // Separator placed between concatenated source label values. Defaults to ';'. + Separator string `json:"separator,omitempty"` + // Label to which the resulting value is written in a replace action. + // It is mandatory for replace actions. Regex capture groups are available. + // +kubebuilder:validation:Pattern=^[a-zA-Z_][a-zA-Z0-9_]*$ + // +kubebuilder:validation:MaxLength:100 + // +kubebuilder:validation:XValidation:rule="self != 'project_id' && self != 'location' && self != 'cluster' && self != 'namespace' && self != 'job' && self != 'instance' && self != 'top_level_controller' && self != 'top_level_controller_type' && self != '__address__'",messageExpression="'cannot relabel onto protected label \"%s\"'.format([self])" + TargetLabel string `json:"targetLabel,omitempty"` + // Regular expression against which the extracted value is matched. Defaults to '(.*)'. + // +kubebuilder:validation:MaxLength=10000 + Regex string `json:"regex,omitempty"` + // Modulus to take of the hash of the source label values. + Modulus uint64 `json:"modulus,omitempty"` + // Replacement value against which a regex replace is performed if the + // regular expression matches. Regex capture groups are available. Defaults to '$1'. + Replacement string `json:"replacement,omitempty"` + // Action to perform based on regex matching. Defaults to 'replace'. + // +kubebuilder:validation:Enum=replace;lowercase;uppercase;keep;drop;keepequal;dropequal;hashmod;labeldrop;labelkeep + Action string `json:"action,omitempty"` +} + +// Rule is a single rule in the Prometheus format: +// https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ +// +kubebuilder:validation:XValidation:rule="(has(self.record) ? 1 : 0) + (has(self.alert) ? 1 : 0) == 1",message="Must set exactly one of Record or Alert" +// +kubebuilder:validation:XValidation:rule="!has(self.annotations) || has(self.alert)",message="Annotations are only allowed for alerting rules" +type Rule struct { + // Record the result of the expression to this metric name. + // Only one of `record` and `alert` must be set. + // +kubebuilder:validation:Pattern="^[a-zA-Z_:][a-zA-Z0-9_:]*$" + Record string `json:"record,omitempty"` + // Name of the alert to evaluate the expression as. + // Only one of `record` and `alert` must be set. + Alert string `json:"alert,omitempty"` + // The PromQL expression to evaluate. + Expr string `json:"expr"` + // The duration to wait before a firing alert produced by this rule is sent to Alertmanager. + // Only valid if `alert` is set. + // +kubebuilder:validation:Format=duration + For string `json:"for,omitempty"` + // A set of labels to attach to the result of the query expression. + Labels map[string]string `json:"labels,omitempty"` + // A set of annotations to attach to alerts produced by the query expression. + // Only valid if `alert` is set. + Annotations map[string]string `json:"annotations,omitempty"` +} + +// RuleGroup declares rules in the Prometheus format: +// https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/ +type RuleGroup struct { + // The name of the rule group. + Name string `json:"name"` + // The interval at which to evaluate the rules. Must be a valid Prometheus duration. + // +kubebuilder:validation:Format=duration + // +kubebuilder:default="1m" + Interval string `json:"interval,omitempty"` + // A list of rules that are executed sequentially as part of this group. + // +kubebuilder:validation:MinItems=1 + Rules []Rule `json:"rules"` +} + +// Rules defines Prometheus alerting and recording rules that are scoped +// to the namespace of the resource. Only metric data from this namespace is processed +// and all rule results have their project_id, cluster, and namespace label preserved +// for query processing. +// If the location label is not preserved by the rule, it defaults to the cluster's location. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +type Rules struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of rules to record and alert on. + Spec RulesSpec `json:"spec"` + // Most recently observed status of the resource. + // +optional + Status RulesStatus `json:"status"` +} + +// RulesList is a list of Rules. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type RulesList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Rules `json:"items"` +} + +// RulesSpec contains specification parameters for a Rules resource. +type RulesSpec struct { + // A list of Prometheus rule groups. + Groups []RuleGroup `json:"groups"` +} + +// RulesStatus contains status information for a Rules resource. +type RulesStatus struct { + MonitoringStatus `json:",inline"` +} + +type SampleGroup struct { + // Targets emitting the error message. + SampleTargets []SampleTarget `json:"sampleTargets,omitempty"` + // Total count of similar errors. + // +optional + Count *int32 `json:"count,omitempty"` +} + +type SampleTarget struct { + // The label set, keys and values, of the target. + Labels prommodel.LabelSet `json:"labels,omitempty"` + // Error message. + LastError *string `json:"lastError,omitempty"` + // Scrape duration in seconds. + LastScrapeDurationSeconds string `json:"lastScrapeDurationSeconds,omitempty"` + // Health status. + Health string `json:"health,omitempty"` +} + +// ScrapeEndpoint specifies a Prometheus metrics endpoint to scrape. +// +kubebuilder:validation:XValidation:rule="!has(self.timeout) || self.timeout <= self.interval",messageExpression="'scrape timeout (%s) must not be greater than scrape interval (%s)'.format([self.timeout, self.interval])" +type ScrapeEndpoint struct { + // Name or number of the port to scrape. + // The container metadata label is only populated if the port is referenced by name + // because port numbers are not unique across containers. + // +kubebuilder:validation:XIntOrString + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="self != 0",message="Port is required" + // +required + Port intstr.IntOrString `json:"port,omitempty"` + // Protocol scheme to use to scrape. + // +kubebuilder:validation:Enum=http;https + Scheme string `json:"scheme,omitempty"` + // HTTP path to scrape metrics from. Defaults to "/metrics". + Path string `json:"path,omitempty"` + // HTTP GET params to use when scraping. + Params map[string][]string `json:"params,omitempty"` + // Interval at which to scrape metrics. Must be a valid Prometheus duration. + // +kubebuilder:validation:Format=duration + // +required + Interval string `json:"interval"` + // Timeout for metrics scrapes. Must be a valid Prometheus duration. + // Must not be larger than the scrape interval. + // +kubebuilder:validation:Format=duration + Timeout string `json:"timeout,omitempty"` + // Relabeling rules for metrics scraped from this endpoint. Relabeling rules that + // override protected target labels (project_id, location, cluster, namespace, job, + // instance, top_level_controller, top_level_controller_type, or __address__) are + // not permitted. The labelmap action is not permitted in general. + // +kubebuilder:validation:MaxItems=250 + MetricRelabeling []RelabelingRule `json:"metricRelabeling,omitempty"` + // Prometheus HTTP client configuration. + HTTPClientConfig `json:",inline"` +} + +type ScrapeEndpointStatus struct { + // The name of the ScrapeEndpoint. + Name string `json:"name"` + // Total number of active targets. + ActiveTargets int64 `json:"activeTargets,omitempty"` + // Total number of active, unhealthy targets. + UnhealthyTargets int64 `json:"unhealthyTargets,omitempty"` + // Last time this status was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` + // A fixed sample of targets grouped by error type. + SampleGroups []SampleGroup `json:"sampleGroups,omitempty"` + // Fraction of collectors included in status, bounded [0,1]. + // Ideally, this should always be 1. Anything less can + // be considered a problem and should be investigated. + CollectorsFraction string `json:"collectorsFraction,omitempty"` +} + +// ScrapeLimits limits applied to scraped targets. +type ScrapeLimits struct { + // Maximum number of samples accepted within a single scrape. + // Uses Prometheus default if left unspecified. + Samples uint64 `json:"samples,omitempty"` + // Maximum number of labels accepted for a single sample. + // Uses Prometheus default if left unspecified. + Labels uint64 `json:"labels,omitempty"` + // Maximum label name length. + // Uses Prometheus default if left unspecified. + LabelNameLength uint64 `json:"labelNameLength,omitempty"` + // Maximum label value length. + // Uses Prometheus default if left unspecified. + LabelValueLength uint64 `json:"labelValueLength,omitempty"` +} + +// SecretKeySelector represents selector for Kubernetes secret. +// It's similar to k8s.io/api/core/v1.SecretKeySelector, but allows cross namespace selections. +type SecretKeySelector struct { + // Name of the secret to select from. + Name string `json:"name"` + + // Key of the secret to select from. Must be a valid secret key. + Key string `json:"key"` + + // Namespace of the secret to select from. + // If empty the parent resource namespace will be chosen. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// SecretSelector references a secret from a secret provider e.g. Kubernetes Secret. Only one +// provider can be used at a time. +type SecretSelector struct { + // Secret represents reference to a given key from certain Secret in a given namespace. + // +optional + Secret *SecretKeySelector `json:"secret,omitempty"` +} + +// TLS specifies TLS configuration used for HTTP requests. +// +kubebuilder:validation:XValidation:rule=has(self.cert) == has(self.key),message="client cert and client key must be provided together, when either is provided" +type TLS struct { + // ServerName is used to verify the hostname for the targets. + // +optional + ServerName string `json:"serverName,omitempty"` + // InsecureSkipVerify disables target certificate validation. + // +optional + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` + // MinVersion is the minimum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), + // TLS12 (TLS 1.2), TLS13 (TLS 1.3). + // + // If unset, Prometheus will use Go default minimum version, which is TLS 1.2. + // See MinVersion in https://pkg.go.dev/crypto/tls#Config. + // +kubebuilder:validation:Enum=TLS10;TLS11;TLS12;TLS13 + // +optional + MinVersion string `json:"minVersion,omitempty"` + // MaxVersion is the maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), + // TLS12 (TLS 1.2), TLS13 (TLS 1.3). + // + // If unset, Prometheus will use Go default minimum version, which is TLS 1.2. + // See MinVersion in https://pkg.go.dev/crypto/tls#Config. + // +kubebuilder:validation:Enum=TLS10;TLS11;TLS12;TLS13 + // +optional + MaxVersion string `json:"maxVersion,omitempty"` + + // CA uses the secret as the CA certificate to validate the server with. + // +optional + + CA *SecretSelector `json:"ca,omitempty"` + // Cert uses the secret as the certificate for client authentication to the server. + // +optional + Cert *SecretSelector `json:"cert,omitempty"` + // Key uses the secret as the private key for client authentication to the server. + // +optional + Key *SecretSelector `json:"key,omitempty"` +} + +// TargetLabels configures labels for the discovered Prometheus targets. +type TargetLabels struct { + // Pod metadata labels that are set on all scraped targets. + // Permitted keys are `container`, `node`, `pod`, `top_level_controller_name`, + // and `top_level_controller_type`. The `container` + // label is only populated if the scrape port is referenced by name. + // Defaults to [container, pod, top_level_controller_name, top_level_controller_type]. + // If set to null, it will be interpreted as the empty list. + // This is for backwards-compatibility only. + // +kubebuilder:validation:items:Enum=container;node;pod;top_level_controller_name;top_level_controller_type + // +listType=set + // +kubebuilder:default=container;pod;top_level_controller_name;top_level_controller_type + Metadata *[]string `json:"metadata,omitempty"` + // Labels to transfer from the Kubernetes Pod to Prometheus target labels. + // Mappings are applied in order. + // +kubebuilder:validation:MaxItems=100 + FromPod []LabelMapping `json:"fromPod,omitempty"` +} diff --git a/support/gmpmonitoring/types_test.go b/support/gmpmonitoring/types_test.go new file mode 100644 index 000000000000..b4e4080fdfc5 --- /dev/null +++ b/support/gmpmonitoring/types_test.go @@ -0,0 +1,232 @@ +package gmpmonitoring_test + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + . "github.com/onsi/gomega" + + hyperapi "github.com/openshift/hypershift/support/api" + gmp "github.com/openshift/hypershift/support/gmpmonitoring" + "github.com/openshift/hypershift/support/rhobsmonitoring" + + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" + + "sigs.k8s.io/yaml" + + "github.com/go-openapi/spec" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" + "github.com/prometheus/common/model" +) + +func monitoringFixtures() (*gmp.PodMonitoring, *gmp.Rules) { + metadata := []string{"pod", "container"} + running, count, lastError := true, int32(1), "connection refused" + secret := func(name, key string) *gmp.SecretSelector { + return &gmp.SecretSelector{Secret: &gmp.SecretKeySelector{Name: name, Key: key}} + } + timestamp := metav1.NewTime(time.Date(2026, time.October, 13, 12, 0, 0, 0, time.UTC)) + status := gmp.MonitoringStatus{ObservedGeneration: 2, Conditions: []gmp.MonitoringCondition{{Type: gmp.ConfigurationCreateSuccess, Status: corev1.ConditionTrue, Reason: "ValidConfig", LastUpdateTime: timestamp, LastTransitionTime: timestamp}}} + monitor := &gmp.PodMonitoring{ + TypeMeta: metav1.TypeMeta{APIVersion: gmp.SchemeGroupVersion.String(), Kind: "PodMonitoring"}, + ObjectMeta: metav1.ObjectMeta{Name: "etcd", Namespace: "clusters-gmp", Labels: map[string]string{"component": "etcd"}}, + Spec: gmp.PodMonitoringSpec{ + Selector: metav1.LabelSelector{MatchLabels: map[string]string{"app": "etcd"}, MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "role", Operator: metav1.LabelSelectorOpIn, Values: []string{"control-plane"}}}}, + FilterRunning: &running, + Limits: &gmp.ScrapeLimits{Samples: 10000, Labels: 100, LabelNameLength: 100, LabelValueLength: 1000}, + TargetLabels: gmp.TargetLabels{Metadata: &metadata, FromPod: []gmp.LabelMapping{{From: "app", To: "component"}}}, + Endpoints: []gmp.ScrapeEndpoint{{ + Port: intstr.FromString("metrics"), Interval: "30s", Timeout: "10s", Scheme: "https", Path: "/metrics", Params: map[string][]string{"format": {"prometheus"}}, + MetricRelabeling: []gmp.RelabelingRule{{Action: "keep", SourceLabels: []string{"__name__"}, Regex: "etcd_.*"}}, + HTTPClientConfig: gmp.HTTPClientConfig{TLS: &gmp.TLS{ServerName: "etcd-client", MinVersion: "TLS12", CA: secret("gmp-etcd-ca", "ca.crt"), Cert: secret("etcd-metrics-client-tls", "etcd-client.crt"), Key: secret("etcd-metrics-client-tls", "etcd-client.key")}}, + }}, + }, + Status: gmp.PodMonitoringStatus{MonitoringStatus: status, EndpointStatuses: []gmp.ScrapeEndpointStatus{{Name: "metrics", LastUpdateTime: timestamp, ActiveTargets: 3, UnhealthyTargets: 1, CollectorsFraction: "1", SampleGroups: []gmp.SampleGroup{{Count: &count, SampleTargets: []gmp.SampleTarget{{Labels: model.LabelSet{"pod": "etcd-0"}, LastError: &lastError, LastScrapeDurationSeconds: "0.1", Health: "down"}}}}}}}, + } + rules := &gmp.Rules{ + TypeMeta: metav1.TypeMeta{APIVersion: gmp.SchemeGroupVersion.String(), Kind: "Rules"}, + ObjectMeta: metav1.ObjectMeta{Name: "etcd", Namespace: monitor.Namespace}, + Spec: gmp.RulesSpec{Groups: []gmp.RuleGroup{{Name: "etcd", Interval: "30s", Rules: []gmp.Rule{ + {Record: "etcd:database_size_bytes:sum", Expr: "sum(etcd_mvcc_db_total_size_in_bytes)", Labels: map[string]string{"component": "etcd"}}, + {Alert: "EtcdDatabaseLarge", Expr: "etcd_mvcc_db_total_size_in_bytes > 1e9", For: "5m", Labels: map[string]string{"severity": "warning"}, Annotations: map[string]string{"summary": "Database size is high"}}, + }}}}, + Status: gmp.RulesStatus{MonitoringStatus: status}, + } + return monitor, rules +} + +func TestAddToScheme(t *testing.T) { + g := NewWithT(t) + scheme := runtime.NewScheme() + g.Expect(gmp.AddToScheme(scheme)).To(Succeed()) + for _, kind := range []string{"PodMonitoring", "PodMonitoringList", "Rules", "RulesList"} { + t.Run("When "+kind+" is registered, it should construct a Kubernetes object", func(t *testing.T) { + g := NewWithT(t) + obj, err := scheme.New(gmp.SchemeGroupVersion.WithKind(kind)) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj.DeepCopyObject()).ToNot(BeNil()) + }) + } + g.Expect(scheme.Recognizes(gmp.SchemeGroupVersion.WithKind("ClusterPodMonitoring"))).To(BeFalse()) + g.Expect(scheme.Recognizes(gmp.SchemeGroupVersion.WithKind("ClusterRules"))).To(BeFalse()) +} + +func TestDeepCopy(t *testing.T) { + g := NewWithT(t) + monitor, rules := monitoringFixtures() + originalMonitor, originalRules := monitor.DeepCopy(), rules.DeepCopy() + copy := monitor.DeepCopy() + copy.Labels["component"] = "changed" + copy.Spec.Selector.MatchLabels["app"] = "changed" + copy.Spec.Selector.MatchExpressions[0].Values[0] = "changed" + *copy.Spec.FilterRunning = false + copy.Spec.Limits.Samples = 1 + (*copy.Spec.TargetLabels.Metadata)[0] = "node" + copy.Spec.TargetLabels.FromPod[0].From = "changed" + copy.Spec.Endpoints[0].Params["format"][0] = "changed" + copy.Spec.Endpoints[0].TLS.CA.Secret.Name = "changed" + copy.Spec.Endpoints[0].TLS.Cert.Secret.Key = "changed" + copy.Spec.Endpoints[0].TLS.Key.Secret.Key = "changed" + copy.Spec.Endpoints[0].MetricRelabeling[0].SourceLabels[0] = "changed" + copy.Status.Conditions[0].Reason = "changed" + group := ©.Status.EndpointStatuses[0].SampleGroups[0] + *group.Count = 99 + group.SampleTargets[0].Labels["pod"] = "changed" + *group.SampleTargets[0].LastError = "changed" + g.Expect(monitor).To(Equal(originalMonitor)) + copiedRules := rules.DeepCopy() + copiedRules.Spec.Groups[0].Rules[0].Labels["component"] = "changed" + copiedRules.Spec.Groups[0].Rules[1].Annotations["summary"] = "changed" + copiedRules.Status.Conditions[0].Reason = "changed" + g.Expect(rules).To(Equal(originalRules)) + monitorList := &gmp.PodMonitoringList{Items: []gmp.PodMonitoring{*monitor}} + monitorList.DeepCopy().Items[0].Spec.Endpoints[0].TLS.CA.Secret.Name = "changed" + g.Expect(monitorList.Items[0]).To(Equal(*originalMonitor)) + rulesList := &gmp.RulesList{Items: []gmp.Rules{*rules}} + rulesList.DeepCopy().Items[0].Spec.Groups[0].Rules[0].Labels["component"] = "changed" + g.Expect(rulesList.Items[0]).To(Equal(*originalRules)) +} + +func TestCompatibleYAMLEncode(t *testing.T) { + monitor, rules := monitoringFixtures() + objects := []runtime.Object{monitor, rules, + &gmp.PodMonitoringList{TypeMeta: metav1.TypeMeta{APIVersion: gmp.SchemeGroupVersion.String(), Kind: "PodMonitoringList"}, Items: []gmp.PodMonitoring{*monitor}}, + &gmp.RulesList{TypeMeta: metav1.TypeMeta{APIVersion: gmp.SchemeGroupVersion.String(), Kind: "RulesList"}, Items: []gmp.Rules{*rules}}, + } + for _, obj := range objects { + t.Run("When "+obj.GetObjectKind().GroupVersionKind().Kind+" is serialized, it should round trip specs and status", func(t *testing.T) { + g := NewWithT(t) + for _, serializer := range []*runtimeSerializer{{encode: func(obj runtime.Object) ([]byte, error) { + return hyperapi.CompatibleYAMLEncode(obj, hyperapi.YamlSerializer) + }, decode: hyperapi.YamlSerializer.Decode}, + {encode: func(obj runtime.Object) ([]byte, error) { + var buf bytes.Buffer + err := hyperapi.JsonSerializer.Encode(obj, &buf) + return buf.Bytes(), err + }, decode: hyperapi.JsonSerializer.Decode}} { + encoded, err := serializer.encode(obj.DeepCopyObject()) + g.Expect(err).ToNot(HaveOccurred()) + decoded, gvk, err := serializer.decode(encoded, nil, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(gvk.GroupVersion()).To(Equal(gmp.SchemeGroupVersion)) + g.Expect(apiequality.Semantic.DeepEqual(obj, decoded)).To(BeTrue(), "round trip changed %T", obj) + decodedAll, _, err := hyperapi.AllMonitoringYamlSerializer.Decode(encoded, nil, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(apiequality.Semantic.DeepEqual(obj, decodedAll)).To(BeTrue(), "all-monitoring round trip changed %T", obj) + } + }) + } +} + +type runtimeSerializer struct { + encode func(runtime.Object) ([]byte, error) + decode func([]byte, *schema.GroupVersionKind, runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) +} + +func TestValidateSchema(t *testing.T) { + monitor, rules := monitoringFixtures() + for _, tc := range []struct { + name, fixture string + object runtime.Object + invalid func(map[string]interface{}) + }{ + {name: "When a typed PodMonitoring is emitted, it should conform to the upstream schema", fixture: "podmonitorings", object: monitor, invalid: func(obj map[string]interface{}) { obj["spec"].(map[string]interface{})["endpoints"] = []interface{}{} }}, + {name: "When typed Rules are emitted, it should conform to the upstream schema", fixture: "rules", object: rules, invalid: func(obj map[string]interface{}) { delete(obj["spec"].(map[string]interface{}), "groups") }}, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + fixture, err := os.ReadFile(filepath.Join("testdata", tc.fixture+".crd.yaml")) + g.Expect(err).ToNot(HaveOccurred()) + var crd struct { + Spec struct { + Versions []struct { + Name string + Schema struct{ OpenAPIV3Schema spec.Schema } + } + } + } + g.Expect(yaml.Unmarshal(fixture, &crd)).To(Succeed()) + var schema *spec.Schema + for i := range crd.Spec.Versions { + if crd.Spec.Versions[i].Name == "v1" { + schema = &crd.Spec.Versions[i].Schema.OpenAPIV3Schema + } + } + g.Expect(schema).ToNot(BeNil()) + encoded, err := json.Marshal(tc.object) + g.Expect(err).ToNot(HaveOccurred()) + var obj map[string]interface{} + g.Expect(json.Unmarshal(encoded, &obj)).To(Succeed()) + g.Expect(validate.AgainstSchema(schema, obj, strfmt.Default)).To(Succeed()) + tc.invalid(obj) + g.Expect(validate.AgainstSchema(schema, obj, strfmt.Default)).ToNot(Succeed(), "negative control must be rejected") + }) + } +} + +func TestSchemeInitialization(t *testing.T) { + if os.Getenv("GMP_SCHEME_TEST_CHILD") == "1" { + g := NewWithT(t) + for _, scheme := range []*runtime.Scheme{hyperapi.Scheme, hyperapi.AllMonitoringScheme} { + g.Expect(scheme.Recognizes(gmp.SchemeGroupVersion.WithKind("PodMonitoring"))).To(BeTrue()) + g.Expect(scheme.Recognizes(gmp.SchemeGroupVersion.WithKind("Rules"))).To(BeTrue()) + } + coreos := schema.GroupVersion{Group: "monitoring.coreos.com", Version: "v1"}.WithKind("ServiceMonitor") + rhobs := rhobsmonitoring.SchemeGroupVersion.WithKind("ServiceMonitor") + rhobsEnabled := os.Getenv(rhobsmonitoring.EnvironmentVariable) == "1" + g.Expect(hyperapi.Scheme.Recognizes(coreos)).To(Equal(!rhobsEnabled)) + g.Expect(hyperapi.Scheme.Recognizes(rhobs)).To(Equal(rhobsEnabled)) + g.Expect(hyperapi.AllMonitoringScheme.Recognizes(coreos)).To(BeTrue()) + g.Expect(hyperapi.AllMonitoringScheme.Recognizes(rhobs)).To(Equal(rhobsEnabled)) + return + } + for _, tc := range []struct{ name, rhobs string }{ + {name: "When the default process initializes, it should register GMP alongside CoreOS"}, + {name: "When a RHOBS process initializes, it should register GMP alongside RHOBS", rhobs: "1"}, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestSchemeInitialization$") + for _, env := range os.Environ() { + if !strings.HasPrefix(env, rhobsmonitoring.EnvironmentVariable+"=") && !strings.HasPrefix(env, "GMP_SCHEME_TEST_CHILD=") { + cmd.Env = append(cmd.Env, env) + } + } + cmd.Env = append(cmd.Env, "GMP_SCHEME_TEST_CHILD=1", rhobsmonitoring.EnvironmentVariable+"="+tc.rhobs) + output, err := cmd.CombinedOutput() + g.Expect(err).ToNot(HaveOccurred(), "%s", output) + }) + } +} diff --git a/support/gmpmonitoring/upstream.sha256 b/support/gmpmonitoring/upstream.sha256 new file mode 100644 index 000000000000..28a13776724e --- /dev/null +++ b/support/gmpmonitoring/upstream.sha256 @@ -0,0 +1,21 @@ +# Code generated by hack/update-gmp-api.sh; DO NOT EDIT. +# Module: github.com/GoogleCloudPlatform/prometheus-engine +# Version: v0.17.2 +# Revision: 058752ec791edefba5c42486eb9237562c3f0d29 +# Module sum: h1:b4Sau/04Xn0wqGyGABUzLVOSlM7BxsZHbcYEXP3EDeg= +# go.mod sum: h1:Mfy+yPZh97JLpnp8/WADyS3PJ3xzK8tua/luLRYkwtU= +d344029bc7754e0254d7c253db9d6400b344d5837e121b42790946757bc79f1f charts/operator/crds/monitoring.googleapis.com_podmonitorings.yaml +49c2cfea8d228a96c466ac49c09f4c19f6cb51ebdd258011cb364a97cb24afee charts/operator/crds/monitoring.googleapis.com_rules.yaml +925ec984ab400a99bf29a74e51fc7270effd8f315b6b0030d385f2f96f6d565c pkg/operator/apis/monitoring/v1/common_types.go +89dda6427bca1516f1159cfd4d3e1db9853c2c70d10f1096fce84298ac65f053 pkg/operator/apis/monitoring/v1/doc.go +4e5b470f3ba58e0a0e3b53a5148ce12da77560021ff42c122c9b23403f14af34 pkg/operator/apis/monitoring/v1/http_types.go +08aeb27a9e087227a35c9bc21875e0c2f843c63d3904f39ed61506aaac0a1c27 pkg/operator/apis/monitoring/v1/monitoring_types.go +7b3f74fd22d15645339deda5a10d0dc8b925e95a972d299f9a679a7442139fe9 pkg/operator/apis/monitoring/v1/node_types.go +f0003dfdf67c669fe44c330654dc9af4e1eeeae9a6eb462b0aaeb4211fdc4047 pkg/operator/apis/monitoring/v1/operator_admission.go +f1d7b67ce21ddfa040c63e182e1f1f7f7a2242ca24bced495827babc3c45d5b1 pkg/operator/apis/monitoring/v1/operator_config.go +4c285b93b4f9b15d0cabad098c79b93875b8c6f0fb3fc17cd8c385df5a4f3ef4 pkg/operator/apis/monitoring/v1/operator_types.go +36a4f137f330994a10e58f9664e85b82027e4e4ee4e35157838d6345a750478a pkg/operator/apis/monitoring/v1/pod_config.go +0dac0ce3c09f8f3f3381c95bb970b147c18077896b2d3dcec510d5e3e5b4abc5 pkg/operator/apis/monitoring/v1/pod_types.go +9c6eec0b2ca8fe2380796f2754e26f68bf56427749ffbedccda01e057f4413d9 pkg/operator/apis/monitoring/v1/register.go +d6bb86f0b4b1fa157044dba696a96419adc4187b16dfb2a4cedadfc07baf55a2 pkg/operator/apis/monitoring/v1/rules_config.go +182e88bf9d879e7b50383198f1b0f45bf09f7d7d9b43e23f14ab5596280dee96 pkg/operator/apis/monitoring/v1/rules_types.go diff --git a/support/gmpmonitoring/zz_generated.deepcopy.go b/support/gmpmonitoring/zz_generated.deepcopy.go new file mode 100644 index 000000000000..dff2e0c8742b --- /dev/null +++ b/support/gmpmonitoring/zz_generated.deepcopy.go @@ -0,0 +1,719 @@ +//go:build !ignore_autogenerated + +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package gmpmonitoring + +import ( + "github.com/prometheus/common/model" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Auth) DeepCopyInto(out *Auth) { + *out = *in + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = new(SecretSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Auth. +func (in *Auth) DeepCopy() *Auth { + if in == nil { + return nil + } + out := new(Auth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuth) DeepCopyInto(out *BasicAuth) { + *out = *in + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(SecretSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuth. +func (in *BasicAuth) DeepCopy() *BasicAuth { + if in == nil { + return nil + } + out := new(BasicAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPClientConfig) DeepCopyInto(out *HTTPClientConfig) { + *out = *in + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(Auth) + (*in).DeepCopyInto(*out) + } + if in.BasicAuth != nil { + in, out := &in.BasicAuth, &out.BasicAuth + *out = new(BasicAuth) + (*in).DeepCopyInto(*out) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLS) + (*in).DeepCopyInto(*out) + } + if in.OAuth2 != nil { + in, out := &in.OAuth2, &out.OAuth2 + *out = new(OAuth2) + (*in).DeepCopyInto(*out) + } + out.ProxyConfig = in.ProxyConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPClientConfig. +func (in *HTTPClientConfig) DeepCopy() *HTTPClientConfig { + if in == nil { + return nil + } + out := new(HTTPClientConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LabelMapping) DeepCopyInto(out *LabelMapping) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelMapping. +func (in *LabelMapping) DeepCopy() *LabelMapping { + if in == nil { + return nil + } + out := new(LabelMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonitoringCondition) DeepCopyInto(out *MonitoringCondition) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonitoringCondition. +func (in *MonitoringCondition) DeepCopy() *MonitoringCondition { + if in == nil { + return nil + } + out := new(MonitoringCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MonitoringStatus) DeepCopyInto(out *MonitoringStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]MonitoringCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MonitoringStatus. +func (in *MonitoringStatus) DeepCopy() *MonitoringStatus { + if in == nil { + return nil + } + out := new(MonitoringStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OAuth2) DeepCopyInto(out *OAuth2) { + *out = *in + if in.ClientSecret != nil { + in, out := &in.ClientSecret, &out.ClientSecret + *out = new(SecretSelector) + (*in).DeepCopyInto(*out) + } + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EndpointParams != nil { + in, out := &in.EndpointParams, &out.EndpointParams + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLS) + (*in).DeepCopyInto(*out) + } + out.ProxyConfig = in.ProxyConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2. +func (in *OAuth2) DeepCopy() *OAuth2 { + if in == nil { + return nil + } + out := new(OAuth2) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodMonitoring) DeepCopyInto(out *PodMonitoring) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodMonitoring. +func (in *PodMonitoring) DeepCopy() *PodMonitoring { + if in == nil { + return nil + } + out := new(PodMonitoring) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PodMonitoring) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodMonitoringList) DeepCopyInto(out *PodMonitoringList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PodMonitoring, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodMonitoringList. +func (in *PodMonitoringList) DeepCopy() *PodMonitoringList { + if in == nil { + return nil + } + out := new(PodMonitoringList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PodMonitoringList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodMonitoringSpec) DeepCopyInto(out *PodMonitoringSpec) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]ScrapeEndpoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.TargetLabels.DeepCopyInto(&out.TargetLabels) + if in.Limits != nil { + in, out := &in.Limits, &out.Limits + *out = new(ScrapeLimits) + **out = **in + } + if in.FilterRunning != nil { + in, out := &in.FilterRunning, &out.FilterRunning + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodMonitoringSpec. +func (in *PodMonitoringSpec) DeepCopy() *PodMonitoringSpec { + if in == nil { + return nil + } + out := new(PodMonitoringSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodMonitoringStatus) DeepCopyInto(out *PodMonitoringStatus) { + *out = *in + in.MonitoringStatus.DeepCopyInto(&out.MonitoringStatus) + if in.EndpointStatuses != nil { + in, out := &in.EndpointStatuses, &out.EndpointStatuses + *out = make([]ScrapeEndpointStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodMonitoringStatus. +func (in *PodMonitoringStatus) DeepCopy() *PodMonitoringStatus { + if in == nil { + return nil + } + out := new(PodMonitoringStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyConfig) DeepCopyInto(out *ProxyConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfig. +func (in *ProxyConfig) DeepCopy() *ProxyConfig { + if in == nil { + return nil + } + out := new(ProxyConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RelabelingRule) DeepCopyInto(out *RelabelingRule) { + *out = *in + if in.SourceLabels != nil { + in, out := &in.SourceLabels, &out.SourceLabels + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelabelingRule. +func (in *RelabelingRule) DeepCopy() *RelabelingRule { + if in == nil { + return nil + } + out := new(RelabelingRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rule) DeepCopyInto(out *Rule) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rule. +func (in *Rule) DeepCopy() *Rule { + if in == nil { + return nil + } + out := new(Rule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RuleGroup) DeepCopyInto(out *RuleGroup) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]Rule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RuleGroup. +func (in *RuleGroup) DeepCopy() *RuleGroup { + if in == nil { + return nil + } + out := new(RuleGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rules) DeepCopyInto(out *Rules) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rules. +func (in *Rules) DeepCopy() *Rules { + if in == nil { + return nil + } + out := new(Rules) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Rules) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesList) DeepCopyInto(out *RulesList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Rules, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesList. +func (in *RulesList) DeepCopy() *RulesList { + if in == nil { + return nil + } + out := new(RulesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RulesList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesSpec) DeepCopyInto(out *RulesSpec) { + *out = *in + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = make([]RuleGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesSpec. +func (in *RulesSpec) DeepCopy() *RulesSpec { + if in == nil { + return nil + } + out := new(RulesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RulesStatus) DeepCopyInto(out *RulesStatus) { + *out = *in + in.MonitoringStatus.DeepCopyInto(&out.MonitoringStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RulesStatus. +func (in *RulesStatus) DeepCopy() *RulesStatus { + if in == nil { + return nil + } + out := new(RulesStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SampleGroup) DeepCopyInto(out *SampleGroup) { + *out = *in + if in.SampleTargets != nil { + in, out := &in.SampleTargets, &out.SampleTargets + *out = make([]SampleTarget, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Count != nil { + in, out := &in.Count, &out.Count + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SampleGroup. +func (in *SampleGroup) DeepCopy() *SampleGroup { + if in == nil { + return nil + } + out := new(SampleGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SampleTarget) DeepCopyInto(out *SampleTarget) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(model.LabelSet, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SampleTarget. +func (in *SampleTarget) DeepCopy() *SampleTarget { + if in == nil { + return nil + } + out := new(SampleTarget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScrapeEndpoint) DeepCopyInto(out *ScrapeEndpoint) { + *out = *in + out.Port = in.Port + if in.Params != nil { + in, out := &in.Params, &out.Params + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.MetricRelabeling != nil { + in, out := &in.MetricRelabeling, &out.MetricRelabeling + *out = make([]RelabelingRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.HTTPClientConfig.DeepCopyInto(&out.HTTPClientConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScrapeEndpoint. +func (in *ScrapeEndpoint) DeepCopy() *ScrapeEndpoint { + if in == nil { + return nil + } + out := new(ScrapeEndpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScrapeEndpointStatus) DeepCopyInto(out *ScrapeEndpointStatus) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + if in.SampleGroups != nil { + in, out := &in.SampleGroups, &out.SampleGroups + *out = make([]SampleGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScrapeEndpointStatus. +func (in *ScrapeEndpointStatus) DeepCopy() *ScrapeEndpointStatus { + if in == nil { + return nil + } + out := new(ScrapeEndpointStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScrapeLimits) DeepCopyInto(out *ScrapeLimits) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScrapeLimits. +func (in *ScrapeLimits) DeepCopy() *ScrapeLimits { + if in == nil { + return nil + } + out := new(ScrapeLimits) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector. +func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { + if in == nil { + return nil + } + out := new(SecretKeySelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSelector) DeepCopyInto(out *SecretSelector) { + *out = *in + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(SecretKeySelector) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSelector. +func (in *SecretSelector) DeepCopy() *SecretSelector { + if in == nil { + return nil + } + out := new(SecretSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLS) DeepCopyInto(out *TLS) { + *out = *in + if in.CA != nil { + in, out := &in.CA, &out.CA + *out = new(SecretSelector) + (*in).DeepCopyInto(*out) + } + if in.Cert != nil { + in, out := &in.Cert, &out.Cert + *out = new(SecretSelector) + (*in).DeepCopyInto(*out) + } + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = new(SecretSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. +func (in *TLS) DeepCopy() *TLS { + if in == nil { + return nil + } + out := new(TLS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetLabels) DeepCopyInto(out *TargetLabels) { + *out = *in + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = new([]string) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.FromPod != nil { + in, out := &in.FromPod, &out.FromPod + *out = make([]LabelMapping, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetLabels. +func (in *TargetLabels) DeepCopy() *TargetLabels { + if in == nil { + return nil + } + out := new(TargetLabels) + in.DeepCopyInto(out) + return out +} diff --git a/support/metrics/gmp_monitoring.go b/support/metrics/gmp_monitoring.go new file mode 100644 index 000000000000..fdf0bbaba42f --- /dev/null +++ b/support/metrics/gmp_monitoring.go @@ -0,0 +1,14 @@ +package metrics + +import ( + "fmt" +) + +// ValidateGMPMonitoring rejects enabling Google Managed Prometheus monitoring +// together with RHOBS monitoring, since only one monitoring API can be emitted. +func ValidateGMPMonitoring(gmpMonitoring, rhobsMonitoring bool) error { + if gmpMonitoring && rhobsMonitoring { + return fmt.Errorf("GMP monitoring cannot be enabled together with RHOBS monitoring") + } + return nil +} diff --git a/support/metrics/gmp_monitoring_test.go b/support/metrics/gmp_monitoring_test.go new file mode 100644 index 000000000000..61e3d92cbe65 --- /dev/null +++ b/support/metrics/gmp_monitoring_test.go @@ -0,0 +1,30 @@ +package metrics + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestValidateGMPMonitoring(t *testing.T) { + for _, tc := range []struct { + name string + gmp, rhobs bool + errorContains string + }{ + {name: "When neither GMP nor RHOBS is enabled, it should succeed"}, + {name: "When only GMP is enabled, it should succeed", gmp: true}, + {name: "When only RHOBS is enabled, it should succeed", rhobs: true}, + {name: "When GMP and RHOBS are both enabled, it should reject the conflict", gmp: true, rhobs: true, errorContains: "cannot be enabled together with RHOBS monitoring"}, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + err := ValidateGMPMonitoring(tc.gmp, tc.rhobs) + if tc.errorContains != "" { + g.Expect(err).To(MatchError(ContainSubstring(tc.errorContains))) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +}