mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-29 08:07:33 +09:00
Comments carry the mechanism, not the rationale; every argument removed here is in the design proposal or the PR. Where one claim appeared at several sites, the copy at the site that constrains the code stays. Assisted-by: LLM Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
309 lines
15 KiB
Go
309 lines
15 KiB
Go
/*
|
|
Copyright 2024 The Cozystack Authors.
|
|
|
|
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 config
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
|
"github.com/fluxcd/pkg/apis/kustomize"
|
|
|
|
"github.com/cozystack/cozystack/api/v1alpha1"
|
|
)
|
|
|
|
// ReleaseAnnotationPrefix is the prefix shared by every ApplicationDefinition
|
|
// metadata annotation that changes how cozystack-api builds the generated
|
|
// HelmRelease (the four constants below). cozystack-api reads them once, at
|
|
// start-up, so cozystack-controller has to roll the api Deployment when one of
|
|
// them changes; it selects them by this prefix when hashing the definitions.
|
|
const ReleaseAnnotationPrefix = "release.cozystack.io/"
|
|
|
|
// HelmInstallTimeoutAnnotation is the ApplicationDefinition metadata
|
|
// annotation key that overrides the Flux HelmRelease Install.Timeout and
|
|
// Upgrade.Timeout for a given Application kind.
|
|
const HelmInstallTimeoutAnnotation = "release.cozystack.io/helm-install-timeout"
|
|
|
|
// HelmUpgradeTimeoutAnnotation is the ApplicationDefinition metadata
|
|
// annotation key that overrides only the Flux HelmRelease Upgrade.Timeout
|
|
// for a given Application kind. When set it takes precedence over the
|
|
// Upgrade.Timeout value HelmInstallTimeoutAnnotation would otherwise apply,
|
|
// letting a kind keep a short install budget but a longer upgrade budget
|
|
// (or vice versa).
|
|
const HelmUpgradeTimeoutAnnotation = "release.cozystack.io/helm-upgrade-timeout"
|
|
|
|
// HelmInstallDisableWaitAnnotation is the ApplicationDefinition metadata
|
|
// annotation key that sets HelmReleaseSpec.Install.DisableWait and
|
|
// Upgrade.DisableWait to true for a given Application kind. Use when the
|
|
// parent chart emits child HelmReleases that cannot become Ready during
|
|
// the parent's own install (e.g. the Kubernetes Application emits
|
|
// in-tenant addon HelmReleases that have no worker nodes to schedule on
|
|
// until the worker MachineSets come up, plus a main-phase talos-reconcile
|
|
// Job that produces the TalosConfigTemplate those MachineSets clone from).
|
|
// Without DisableWait the helm-controller blocks on the addon HelmReleases
|
|
// becoming Ready, which cannot happen during install, so the release never
|
|
// settles. DisableWait lets it settle while the addon HelmReleases and the
|
|
// reconcile Job converge asynchronously.
|
|
//
|
|
// This takes precedence over spec.release.healthCheckExprs: upstream evaluates
|
|
// CEL health expressions only when the Helm action has wait enabled, so a kind
|
|
// setting both gets no health gating at all, silently. Prefer dropping this
|
|
// annotation over adding expressions that will never run.
|
|
const HelmInstallDisableWaitAnnotation = "release.cozystack.io/helm-install-disable-wait"
|
|
|
|
// HelmServerSideApplyAnnotation is the ApplicationDefinition metadata annotation
|
|
// that overrides the Helm apply strategy for every HelmRelease the kind emits.
|
|
// The platform's helm-controller (v1.5.0) defaults to server-side apply, under
|
|
// which Helm force-owns every field it renders and reverts any value a runtime
|
|
// writer (e.g. an HPA writing the engine CR's /scale subresource) later sets.
|
|
// Setting this to "false" forces client-side apply, under which helm-controller
|
|
// patches a CRD from the previous-vs-new rendered manifest and does not consult
|
|
// live state (it leaves Helm's threeWayMergeForUnstructured at the default), so a
|
|
// field rendered as an unchanged constant yields no patch and the co-writer's
|
|
// live value survives.
|
|
// Required by the postgres read-replica autoscaler, whose seeded spec.instances
|
|
// must not be reverted from KEDA's live /scale value. Accepts "true"/"enabled" or
|
|
// "false"/"disabled" (case-insensitive); empty leaves the helm-controller default.
|
|
//
|
|
// The switch is deliberately kind-wide, not per-instance: it applies to EVERY
|
|
// HelmRelease the kind emits, including the ones that never enable the co-writing
|
|
// feature. That is a considered trade-off, not an oversight:
|
|
// - Lifecycle stability. helm-controller resolves an unset Upgrade.ServerSideApply
|
|
// through "auto" from the last release's apply method, so gating client-side on
|
|
// "autoscaling active" would force an SSA->client-side ownership handoff on the
|
|
// live spec.instances the moment a tenant enables autoscaling - the exact live
|
|
// field handoff this design was chosen to avoid. Keeping the method stable for
|
|
// the kind's whole lifecycle makes enable/dry-run/transition/disable clean
|
|
// two-way merges with no apply-method transition.
|
|
// - Cost, acknowledged. A release that never autoscales is client-side too, so
|
|
// helm-controller no longer re-asserts a chart-rendered field that has drifted
|
|
// on the live CR (client-side diffs previous-vs-new render only, never live
|
|
// state) and driftDetection is not configured. The platform relies on GitOps
|
|
// re-render for correctness rather than SSA drift-correction; a per-instance
|
|
// apply strategy is a follow-up if drift-correction on non-feature releases of
|
|
// the kind is ever required.
|
|
const HelmServerSideApplyAnnotation = "release.cozystack.io/helm-server-side-apply"
|
|
|
|
// helmTimeoutPattern mirrors the CRD validation pattern used by Flux
|
|
// helm-controller on HelmReleaseSpec.Install.Timeout (ms/s/m/h units only).
|
|
// time.ParseDuration accepts ns/us/µs, but Flux rejects them - parsing here
|
|
// with the same shape avoids feeding the controller a value it will later
|
|
// reject at webhook time. See
|
|
// github.com/fluxcd/helm-controller/api/v2 HelmReleaseSpec.Install.Timeout
|
|
// in the go module cache.
|
|
var helmTimeoutPattern = regexp.MustCompile(`^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$`)
|
|
|
|
// ParseHelmTimeoutAnnotation parses the value of a Flux HelmRelease timeout
|
|
// annotation (release.cozystack.io/helm-install-timeout or
|
|
// release.cozystack.io/helm-upgrade-timeout). The empty string is treated as
|
|
// "unset" and returns (0, nil) so callers can leave the corresponding
|
|
// ReleaseConfig field zeroed and let flux defaults apply. Values accepted by
|
|
// time.ParseDuration but rejected by Flux (ns/us/µs) return a helpful
|
|
// error instead of silently parsing and failing later at HelmRelease
|
|
// admission.
|
|
func ParseHelmTimeoutAnnotation(raw string) (time.Duration, error) {
|
|
if raw == "" {
|
|
return 0, nil
|
|
}
|
|
if !helmTimeoutPattern.MatchString(raw) {
|
|
return 0, fmt.Errorf("must match %s (Flux accepts ms/s/m/h units only), got %q",
|
|
helmTimeoutPattern, raw)
|
|
}
|
|
d, err := time.ParseDuration(raw)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("time.ParseDuration(%q): %w", raw, err)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// ParsePositiveDuration parses raw as a time.Duration and rejects malformed
|
|
// or non-positive values. Flux HelmRelease fields (Interval, Timeout,
|
|
// RetryInterval) require strictly positive durations, so a misconfigured
|
|
// flag must fail fast at startup rather than propagating into every HR.
|
|
// Shared between cozystack-operator and cozystack-api so both paths reject
|
|
// the same set of inputs at startup.
|
|
func ParsePositiveDuration(flagName, raw string) (time.Duration, error) {
|
|
d, err := time.ParseDuration(raw)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid duration for %s=%q: %w", flagName, raw, err)
|
|
}
|
|
if d <= 0 {
|
|
return 0, fmt.Errorf("%s must be > 0 (got %q)", flagName, raw)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// ParseHelmInstallDisableWaitAnnotation parses the value of the
|
|
// release.cozystack.io/helm-install-disable-wait annotation. Accepts
|
|
// "true" or "false" (case-insensitive); empty returns (false, nil) so
|
|
// callers can leave HelmInstallDisableWait zero and let flux defaults
|
|
// apply.
|
|
func ParseHelmInstallDisableWaitAnnotation(raw string) (bool, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
switch {
|
|
case raw == "":
|
|
return false, nil
|
|
case strings.EqualFold(raw, "true"):
|
|
return true, nil
|
|
case strings.EqualFold(raw, "false"):
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("must be \"true\" or \"false\", got %q", raw)
|
|
}
|
|
}
|
|
|
|
// ParseHelmServerSideApplyAnnotation parses the value of the
|
|
// release.cozystack.io/helm-server-side-apply annotation. Returns a pointer to the
|
|
// desired serverSideApply state, or nil for an empty value (leave the helm-controller
|
|
// default). Accepts "true"/"enabled" and "false"/"disabled" (case-insensitive).
|
|
func ParseHelmServerSideApplyAnnotation(raw string) (*bool, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
switch {
|
|
case raw == "":
|
|
return nil, nil
|
|
case strings.EqualFold(raw, "true"), strings.EqualFold(raw, "enabled"):
|
|
v := true
|
|
return &v, nil
|
|
case strings.EqualFold(raw, "false"), strings.EqualFold(raw, "disabled"):
|
|
v := false
|
|
return &v, nil
|
|
default:
|
|
return nil, fmt.Errorf("must be \"true\"/\"enabled\" or \"false\"/\"disabled\", got %q", raw)
|
|
}
|
|
}
|
|
|
|
// ResourceConfig represents the structure of the configuration file.
|
|
type ResourceConfig struct {
|
|
Resources []Resource `yaml:"resources"`
|
|
}
|
|
|
|
// Resource describes an individual resource.
|
|
type Resource struct {
|
|
Application ApplicationConfig `yaml:"application"`
|
|
Release ReleaseConfig `yaml:"release"`
|
|
// Credentials is the definition's spec.credentials; nil for a kind that is
|
|
// not converted.
|
|
Credentials *v1alpha1.ApplicationDefinitionCredentials `yaml:"credentials,omitempty"`
|
|
}
|
|
|
|
// ApplicationConfig contains the application settings.
|
|
type ApplicationConfig struct {
|
|
Kind string `yaml:"kind"`
|
|
Singular string `yaml:"singular"`
|
|
Plural string `yaml:"plural"`
|
|
ShortNames []string `yaml:"shortNames"`
|
|
OpenAPISchema string `yaml:"openAPISchema"`
|
|
}
|
|
|
|
// ReleaseConfig contains the release settings.
|
|
type ReleaseConfig struct {
|
|
Prefix string `yaml:"prefix"`
|
|
Labels map[string]string `yaml:"labels"`
|
|
ChartRef ChartRefConfig `yaml:"chartRef"`
|
|
// HelmInstallTimeout is a per-kind override of Install.Timeout and
|
|
// Upgrade.Timeout. When non-zero, it wins over
|
|
// HelmReleaseInstallTimeout / HelmReleaseUpgradeTimeout below.
|
|
// Populated from the release.cozystack.io/helm-install-timeout
|
|
// annotation on the ApplicationDefinition at start-up.
|
|
HelmInstallTimeout time.Duration `yaml:"helmInstallTimeout,omitempty"`
|
|
// HelmUpgradeTimeout is a per-kind override of Upgrade.Timeout only.
|
|
// When non-zero, it wins over both HelmReleaseUpgradeTimeout and
|
|
// the Upgrade.Timeout value HelmInstallTimeout would otherwise apply,
|
|
// so a kind can carry an asymmetric install/upgrade budget. Populated
|
|
// from the release.cozystack.io/helm-upgrade-timeout annotation on the
|
|
// ApplicationDefinition at start-up.
|
|
HelmUpgradeTimeout time.Duration `yaml:"helmUpgradeTimeout,omitempty"`
|
|
// HelmReleaseInterval is the global default for Spec.Interval on
|
|
// HelmReleases generated by cozystack-api. Set from the api server
|
|
// --helmrelease-interval flag; mirrors the cozystack-operator flag of
|
|
// the same name so both HelmRelease-generating paths use the same
|
|
// reconcile cadence.
|
|
HelmReleaseInterval time.Duration `yaml:"helmReleaseInterval,omitempty"`
|
|
// HelmReleaseRetryInterval is the global default for
|
|
// Install/Upgrade.Strategy.RetryInterval. Decoupled from
|
|
// HelmReleaseInterval so failed install/upgrade retries recover fast
|
|
// without polling healthy releases at the same cadence.
|
|
HelmReleaseRetryInterval time.Duration `yaml:"helmReleaseRetryInterval,omitempty"`
|
|
// HelmReleaseInstallTimeout is the global default for
|
|
// Spec.Install.Timeout. Overridden for this Application kind by
|
|
// HelmInstallTimeout from its ApplicationDefinition.
|
|
HelmReleaseInstallTimeout time.Duration `yaml:"helmReleaseInstallTimeout,omitempty"`
|
|
// HelmReleaseUpgradeTimeout is the global default for
|
|
// Spec.Upgrade.Timeout. Overridden for this Application kind by
|
|
// HelmInstallTimeout, or by HelmUpgradeTimeout with higher precedence,
|
|
// from its ApplicationDefinition.
|
|
HelmReleaseUpgradeTimeout time.Duration `yaml:"helmReleaseUpgradeTimeout,omitempty"`
|
|
// HelmReleaseMaxHistory is the global default for Spec.MaxHistory.
|
|
// 0 means unlimited per Helm semantics; matches the cozystack-operator
|
|
// flag of the same name. No omitempty: 0 ("unlimited") must survive a
|
|
// round-trip distinct from an unset field if ReleaseConfig is ever
|
|
// marshalled.
|
|
HelmReleaseMaxHistory int `yaml:"helmReleaseMaxHistory"`
|
|
// HelmInstallDisableWait sets HelmReleaseSpec.Install.DisableWait and
|
|
// Upgrade.DisableWait to true for this Application kind. Populated
|
|
// from the release.cozystack.io/helm-install-disable-wait
|
|
// annotation on the ApplicationDefinition at start-up.
|
|
HelmInstallDisableWait bool `yaml:"helmInstallDisableWait,omitempty"`
|
|
// HelmServerSideApply overrides the Helm apply strategy for this Application
|
|
// kind: nil leaves the helm-controller default (server-side apply on v1.5.0),
|
|
// false forces client-side apply. Populated from the
|
|
// release.cozystack.io/helm-server-side-apply annotation on the
|
|
// ApplicationDefinition at start-up. See HelmServerSideApplyAnnotation.
|
|
HelmServerSideApply *bool `yaml:"helmServerSideApply,omitempty"`
|
|
// WaitStrategy sets HelmReleaseSpec.WaitStrategy.Name (poller|legacy) for
|
|
// this Application kind. Populated from spec.release.waitStrategy on the
|
|
// ApplicationDefinition at start-up. Empty leaves the flux default, unless
|
|
// HealthCheckExprs is set (see ResolveWaitStrategy).
|
|
WaitStrategy string `yaml:"waitStrategy,omitempty"`
|
|
// HealthCheckExprs sets HelmReleaseSpec.HealthCheckExprs — CEL health
|
|
// expressions evaluated (under the poller strategy) against the custom
|
|
// resource(s) this Application renders. Populated from
|
|
// spec.release.healthCheckExprs on the ApplicationDefinition at start-up.
|
|
// json-tagged via kustomize.CustomHealthCheck; ReleaseConfig is
|
|
// json.Marshalled (never yaml round-tripped), so the json tags are honored.
|
|
HealthCheckExprs []kustomize.CustomHealthCheck `yaml:"healthCheckExprs,omitempty"`
|
|
}
|
|
|
|
// ResolveWaitStrategy builds the HelmReleaseSpec.WaitStrategy value shared by
|
|
// both HelmRelease-generating paths (cozystack-api and cozystack-operator).
|
|
// It maps the scalar waitStrategy string onto the upstream {name} object and
|
|
// couples the default: healthCheckExprs are only evaluated under the poller
|
|
// strategy, so when expressions are present and no strategy was set we default
|
|
// to poller. This makes a package that sets only healthCheckExprs
|
|
// self-contained and removes the "must also set waitStrategy: poller" footgun,
|
|
// independent of the controller's global default. Returns nil (leave unset,
|
|
// flux default applies) when neither a strategy nor expressions are given.
|
|
func ResolveWaitStrategy(waitStrategy string, hasHealthCheckExprs bool) *helmv2.WaitStrategy {
|
|
name := waitStrategy
|
|
if name == "" {
|
|
if !hasHealthCheckExprs {
|
|
return nil
|
|
}
|
|
name = string(helmv2.WaitStrategyPoller)
|
|
}
|
|
return &helmv2.WaitStrategy{Name: helmv2.WaitStrategyName(name)}
|
|
}
|
|
|
|
// ChartRefConfig references a Flux source artifact for the Helm chart.
|
|
type ChartRefConfig struct {
|
|
Kind string `yaml:"kind"`
|
|
Name string `yaml:"name"`
|
|
Namespace string `yaml:"namespace"`
|
|
}
|