-
Notifications
You must be signed in to change notification settings - Fork 106
PO to GMP Migration Tool: Orchestrator & CLI Boilerplate #1961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
036f215
70d2b55
105cfdc
051ded3
69eb392
f6b49d0
c0959e9
eb2594a
7aaa3ad
37e8b5e
ffe27fd
4a78f0f
e5e93ac
a36ffaa
e9b4758
c17f8eb
b380a49
a8ccb1f
fc9d074
bdc0f31
c5440d0
46489b6
a17481a
a673119
9966902
eebdbbe
627d1d0
ad15fd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # Copyright 2024 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 | ||
| # | ||
| # 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. | ||
|
|
||
| FROM --platform=$BUILDPLATFORM google-go.pkg.dev/golang:1.26.4@sha256:3444149d0a7e3f7cfb9c2db65f0f75676fe6ad04de3ce72674efb120c08dd1c1 AS buildbase | ||
| ARG TARGETOS | ||
| ARG TARGETARCH | ||
| ARG BUILDARCH | ||
| WORKDIR /app | ||
| COPY charts/values.global.yaml charts/values.global.yaml | ||
| COPY go.mod go.mod | ||
| COPY go.sum go.sum | ||
| COPY tools tools | ||
| # Copy the Go vendor directory only if it exists. Vendor folder will automatically | ||
| # cause 'go build' to use -mod=vendor flag (otherwise -mod=mod is used). | ||
| COPY vendor* vendor | ||
| COPY cmd cmd | ||
| COPY pkg pkg | ||
|
|
||
| ENV GOEXPERIMENT=boringcrypto | ||
| ENV CGO_ENABLED=1 | ||
| ENV GOFIPS140=off | ||
| ENV GOTOOLCHAIN=local | ||
| ENV GOOS=${TARGETOS} | ||
| ENV GOARCH=${TARGETARCH} | ||
| RUN if [ "${TARGETARCH}" = "arm64" ] && [ "${BUILDARCH}" != "arm64" ]; then \ | ||
| apt-get update && apt-get install -y --no-install-recommends \ | ||
| gcc-aarch64-linux-gnu libc6-dev-arm64-cross; \ | ||
| export CC=aarch64-linux-gnu-gcc; \ | ||
| elif [ "${TARGETARCH}" = "amd64" ] && [ "${BUILDARCH}" != "amd64" ]; then \ | ||
| apt-get update && apt-get install -y --no-install-recommends \ | ||
| gcc-x86-64-linux-gnu libc6-dev-amd64-cross; \ | ||
| export CC=x86_64-linux-gnu-gcc; \ | ||
| fi && \ | ||
| GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ | ||
| go build \ | ||
| -ldflags="-X github.com/prometheus/common/version.Version=$(cat charts/values.global.yaml | go tool -modfile="tools/go.mod" yq '.version' ) \ | ||
| -X github.com/prometheus/common/version.BuildDate=$(date --iso-8601=seconds)" \ | ||
| -o gmp-migrate \ | ||
| cmd/gmp-migrate/*.go | ||
|
karthunni marked this conversation as resolved.
|
||
|
|
||
|
|
||
| FROM gke.gcr.io/gke-distroless/libc:gke_distroless_20260307.00_p0@sha256:d5c073079125b887158bb1dd0ee4da49b39a08203c3c96124ee310962dd5aae2 | ||
| COPY --from=buildbase /app/gmp-migrate /bin/gmp-migrate | ||
| ENTRYPOINT ["/bin/gmp-migrate"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright 2026 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 | ||
| // | ||
| // 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. | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
|
|
||
| "github.com/GoogleCloudPlatform/prometheus-engine/pkg/migrate" | ||
| ) | ||
|
|
||
| func main() { | ||
| slog.SetDefault(slog.New(migrate.NewConsoleHandler(os.Stderr))) | ||
|
|
||
| var inputFile string | ||
| flag.StringVar(&inputFile, "file", "", "Input source (YAML file, directory, or '-' for stdin) (Required)") | ||
| flag.StringVar(&inputFile, "f", "", "Input source (YAML file, directory, or '-' for stdin) (Required)") | ||
|
|
||
| flag.Usage = func() { | ||
| fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) | ||
| fmt.Fprint(os.Stderr, "Migrate Prometheus Operator configurations to Google Managed Prometheus (GMP).\n\n") | ||
| flag.PrintDefaults() | ||
| } | ||
| flag.Parse() | ||
|
karthunni marked this conversation as resolved.
|
||
|
|
||
| if inputFile == "" { | ||
|
karthunni marked this conversation as resolved.
Outdated
|
||
| slog.Error("Flag -f / --file is required.") | ||
| flag.Usage() | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| migrator := migrate.NewMigrator() | ||
| report, err := migrator.Run(inputFile) | ||
| if err != nil { | ||
| slog.Error("Migration failed", slog.Any("error", err)) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| // If any resource failed to migrate, exit with a non-zero code. | ||
| if report.FailedCount > 0 { | ||
| slog.Error("Migration completed with failures", slog.Int("failures", report.FailedCount)) | ||
| os.Exit(1) | ||
| } | ||
|
karthunni marked this conversation as resolved.
|
||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| // Copyright 2026 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 | ||
| // | ||
| // 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. | ||
|
|
||
| package migrate | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "log/slog" | ||
| "maps" | ||
| "os" | ||
| "strings" | ||
| "sync" | ||
| ) | ||
|
|
||
| // Custom slog.Levels for status logging. | ||
| const ( | ||
| LevelSuccess slog.Level = slog.LevelInfo - 1 // Level -1 (Standard Info is 0) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason not to use LevelDebug for this?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made the LevelSuccess just for the explicit naming to reduce confusion, but for the sake of simplicity we could map it to LevelDebug instead |
||
| LevelSkipped slog.Level = slog.LevelInfo + 1 // Level 1 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The could probably just be LevelInfo/LevelWarn.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to get rid of the custom level, I would need to add an attribute to the log to flag it's a skip, so I can count skips accordingly in the Handler for the final summary. Also should I continue to log it as
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you count skips outside of the log messages? |
||
| ) | ||
|
|
||
| // ResourceStatus defines the final migration state of an ingested resource. | ||
| type ResourceStatus int | ||
|
|
||
| const ( | ||
| StatusSuccess ResourceStatus = iota // 0 (Migrated Successfully) | ||
| StatusSkipped // 1 (Skipped / Unsupported) | ||
| StatusWarning // 2 (Migrated with Warnings) | ||
| StatusFailed // 3 (Failed) | ||
| ) | ||
|
|
||
| // statusLevels maps slog.Levels to their corresponding ResourceStatus. | ||
| // Levels omitted from this map (like slog.LevelInfo) represent progress logs | ||
| // and are ignored for status tracking. | ||
| var statusLevels = map[slog.Level]ResourceStatus{ | ||
| LevelSuccess: StatusSuccess, | ||
| LevelSkipped: StatusSkipped, | ||
| slog.LevelWarn: StatusWarning, | ||
| slog.LevelError: StatusFailed, | ||
| } | ||
|
|
||
| // loggerState encapsulates the shared, thread-safe state across all handler clones. | ||
| type loggerState struct { | ||
| mu sync.Mutex | ||
| resourceStatuses map[string]ResourceStatus | ||
| } | ||
|
|
||
| // ConsoleHandler is a thread-safe slog.Handler that formats logs for the console (Stderr) | ||
| // and tracks the highest log level seen per resource (for statistics). | ||
| type ConsoleHandler struct { | ||
| out io.Writer | ||
| state *loggerState | ||
| attrs []slog.Attr | ||
| } | ||
|
karthunni marked this conversation as resolved.
|
||
|
|
||
| // NewConsoleHandler creates a new ConsoleHandler. | ||
| func NewConsoleHandler(out io.Writer) *ConsoleHandler { | ||
| if out == nil { | ||
| out = os.Stderr | ||
| } | ||
| return &ConsoleHandler{ | ||
| out: out, | ||
| state: &loggerState{ | ||
| resourceStatuses: make(map[string]ResourceStatus), | ||
| }, | ||
| } | ||
| } | ||
|
karthunni marked this conversation as resolved.
karthunni marked this conversation as resolved.
|
||
|
|
||
| func (h *ConsoleHandler) Enabled(_ context.Context, _ slog.Level) bool { | ||
| return true // Log everything | ||
| } | ||
|
|
||
| func (h *ConsoleHandler) Handle(_ context.Context, r slog.Record) error { | ||
| h.state.mu.Lock() | ||
| defer h.state.mu.Unlock() | ||
|
|
||
| var kind, namespace, name, file string | ||
| var extraAttrs []string | ||
|
|
||
| // Helper to process and categorize attributes | ||
| processAttr := func(a slog.Attr) { | ||
| val := a.Value.Resolve() | ||
| switch a.Key { | ||
| case "kind": | ||
| kind = val.String() | ||
| case "namespace": | ||
| namespace = val.String() | ||
| case "name": | ||
| name = val.String() | ||
| case "file": | ||
| file = val.String() | ||
| default: | ||
| // Collect all other attributes to print at the end of the line | ||
| extraAttrs = append(extraAttrs, fmt.Sprintf("%s=%v", a.Key, val.Any())) | ||
| } | ||
| } | ||
|
karthunni marked this conversation as resolved.
|
||
|
|
||
| // Extract attributes bound to the logger instance | ||
| for _, a := range h.attrs { | ||
| processAttr(a) | ||
| } | ||
|
|
||
| // Extract attributes passed in the individual log call | ||
| r.Attrs(func(a slog.Attr) bool { | ||
| processAttr(a) | ||
| return true | ||
| }) | ||
|
|
||
| // Map slog.Level to string for console output. | ||
| var levelStr string | ||
| switch r.Level { | ||
| case slog.LevelDebug: | ||
| levelStr = "DEBUG" | ||
| case slog.LevelInfo: | ||
| levelStr = "INFO" | ||
| case LevelSuccess: | ||
| levelStr = "SUCCESS" | ||
| case LevelSkipped: | ||
| levelStr = "SKIPPED" | ||
| case slog.LevelWarn: | ||
| levelStr = "WARNING" | ||
| case slog.LevelError: | ||
| levelStr = "ERROR" | ||
| default: | ||
| levelStr = r.Level.String() | ||
| } | ||
|
|
||
| // Format prefix cleanly | ||
| var prefix string | ||
| if file != "" { | ||
| prefix = fmt.Sprintf("[%s] ", file) | ||
| } else if kind != "" && name != "" { | ||
| if namespace == "" { | ||
| prefix = fmt.Sprintf("[%s:%s] ", kind, name) | ||
| } else { | ||
| prefix = fmt.Sprintf("[%s:%s/%s] ", kind, namespace, name) | ||
| } | ||
| } | ||
|
|
||
| // 1. Write formatted log to Stderr (console), appending extra attributes if any. | ||
| var suffix string | ||
| if len(extraAttrs) > 0 { | ||
| suffix = " " + strings.Join(extraAttrs, " ") | ||
| } | ||
| consoleLine := fmt.Sprintf("[%s] %s%s%s\n", levelStr, prefix, r.Message, suffix) | ||
| if _, err := io.WriteString(h.out, consoleLine); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 2. Track the migration status of the resource (for final report) | ||
| // Only update the map if the log level represents an actual status milestone. | ||
| if status, ok := statusLevels[r.Level]; ok { | ||
| if kind != "" && name != "" { | ||
| var key string | ||
| if namespace == "" { | ||
| key = fmt.Sprintf("%s/%s", kind, name) | ||
| } else { | ||
| key = fmt.Sprintf("%s/%s/%s", kind, namespace, name) | ||
| } | ||
| if val, exists := h.state.resourceStatuses[key]; !exists || status > val { | ||
| h.state.resourceStatuses[key] = status | ||
| } | ||
| } else if file != "" { | ||
| // Track file-level log status under the file path key | ||
| if val, exists := h.state.resourceStatuses[file]; !exists || status > val { | ||
| h.state.resourceStatuses[file] = status | ||
| } | ||
| } | ||
| } | ||
|
karthunni marked this conversation as resolved.
karthunni marked this conversation as resolved.
|
||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler { | ||
| // 1. Allocate a brand-new, independent underlying array of exact size | ||
| newAttrs := make([]slog.Attr, len(h.attrs)+len(attrs)) | ||
|
|
||
| // 2. Copy the parent's attributes to the beginning of the new array | ||
| copy(newAttrs, h.attrs) | ||
|
|
||
| // 3. Copy the new attributes to the remaining space, starting at the offset | ||
| copy(newAttrs[len(h.attrs):], attrs) | ||
| return &ConsoleHandler{ | ||
| out: h.out, | ||
| state: h.state, | ||
| attrs: newAttrs, | ||
| } | ||
| } | ||
|
karthunni marked this conversation as resolved.
karthunni marked this conversation as resolved.
karthunni marked this conversation as resolved.
|
||
|
|
||
| func (h *ConsoleHandler) WithGroup(_ string) slog.Handler { | ||
| return h | ||
| } | ||
|
karthunni marked this conversation as resolved.
|
||
|
|
||
| // ResourceStatuses returns a thread-safe copy of the tracked resource statuses. | ||
| func (h *ConsoleHandler) ResourceStatuses() map[string]ResourceStatus { | ||
| h.state.mu.Lock() | ||
| defer h.state.mu.Unlock() | ||
|
|
||
| return maps.Clone(h.state.resourceStatuses) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.