mirror of
https://github.com/kubevela/kubevela.git
synced 2026-05-12 04:16:45 +00:00
* Docs(KEP): Go SDK for X-Definition Authoring (defkit) Introduces KEP proposal for defkit, a Go SDK that enables platform engineers to author X-Definitions using native Go code instead of CUE. Key proposed features: - Fluent builder API for Component, Trait, Policy, and WorkflowStep definitions - Transparent Go-to-CUE compilation - IDE support with autocomplete and type checking - Schema-agnostic resource construction - Collection operations (map, filter, dedupe) - Composable health and status expressions - Addon integration with godef/ folder support - Module dependencies for definition sharing via go get Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(KEP): Examples and minor api changes given in the document Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(KEP): align defkit examples - Fix golang version in CI - Fix variable declaration in example for testing - Add Is() comparison method to status check Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): add security considerations section - Add goal #7 for secure code execution model - Add Security Considerations section covering: - Code execution model (compile-time only, not runtime) - Security benefits over CUE (static analysis, dependency scanning) - Threat model with mitigations Addresses PR feedback about code execution safety. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): add module versioning and definition placement sections - Add Module Versioning section explaining git-based version derivation - Add Definition Placement section covering: - Motivation for placement constraints in multi-cluster environments - Fluent API for placement (RunOn, NotRunOn, label conditions) - Logical combinators (And, Or, Not) - Module-level placement defaults - Placement evaluation logic - CLI experience for managing cluster labels - Add Module Hooks section for lifecycle callbacks - Minor fixes and clarifications throughout Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): add module hooks and update addon integration sections - Add Module Hooks section covering: - Use cases (CRD installation, setup scripts, post-install samples) - Hook configuration in module.yaml (pre-apply, post-apply) - Hook types (path for manifests, script for shell scripts) - waitFor field with condition names and CUE expressions - CLI usage (--skip-hooks, --dry-run) - Update Addon Integration section with implementation details: - godef/ folder structure with module.yaml - CLI flags (--godef, --components, --traits, --policies, --workflowsteps) - Conflict detection and --override-definitions flag - Development workflow Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): address PR review comments and clarify placement labels - Fix misleading "Sandboxed Compilation" claim (cubic-ai feedback) - renamed to "Isolated Compilation" and clarified that security relies on trust model, not technical sandboxing - Fix inconsistent apiVersion in module hooks example (defkit.oam.dev/v1 → core.oam.dev/v1beta1) - Clarify that placement uses vela-cluster-identity ConfigMap directly, not the vela cluster labels command (which is planned for future) - Add --stats flag to apply-module CLI documentation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(KEP): fix API documentation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add core fluent API types for Go-based definitions Introduce the defkit package providing a fluent Go API for defining KubeVela X-Definitions (components, traits, policies, workflow steps). Core types added: - types.go: Value, Condition, Param interfaces - base.go: Base definition types and interfaces - param.go: Parameter builders (String, Int, Bool, Array, Map, Struct, Enum) - expr.go: Expression builders for conditions and comparisons - resource.go: Resource operations (Set, SetIf, Spread) - context.go: KubeVela context references (appName, namespace, etc.) - test_context.go: Test utilities for definition validation This enables writing type-safe Go definitions that compile to CUE. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add collection operations and helper builders Add fluent API for array/collection transformations: - CollectionOp with Filter, Map, Pick, Wrap, Dedupe operations - From() and Each() entry points for collection pipelines - FieldRef, FieldEquals, FieldMap for field-level operations - MultiSource for complex multi-array comprehensions - Add helper builders for template variables - Add value transformation utilities Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add CUE code generator Implement CUEGenerator that transforms Go definitions into CUE code Added helper methods and writers for conversion Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add status and health policy builders Add fluent builders for customStatus and healthPolicy CUE generation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add definition type builders Add fluent builders for all four KubeVela X-Definition types: - ComponentDefinition - TraitDefinition - PolicyDefinition - WorkflowStepDefinition Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(goloader): add Go module loader for definitions - Definition interface and registry for runtime discovery - Discover and parse Go-based definition files - Compile Go definitions to CUE at runtime - Module environment for batch processing - Parallel generation for better performance Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(cli): add vela def commands for Go-based definitions - init-module: scaffold a new Go definition module - apply-module: compile and apply definitions to cluster - list-module: show definitions in a module - validate-module: validate definitions without applying - Also support the cue commands for xdefintions for go code Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add testing utilities and matchers - CUE comparison matchers for Ginkgo/Gomega tests - Test helpers for definition validation Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add patch container helpers for container mod operations Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(cli): update the go module to 1.23.8 for defkit init-module command Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Refactor: Add grouped help output for vela def command Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add definition placement for cluster-aware deployments Enable definitions to specify which clusters they should run on based on cluster identity labels stored in a well-known ConfigMap. Also derives module version from git tags and improves init-module to create directories from --name flag. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add RunOn/NotRunOn fluent API for placement constraints Add placement methods to all definition builders allowing definitions to specify cluster eligibility using the placement package's fluent API. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Docs(defkit): add commented placement example to module.yaml template Show users the placement syntax in generated module.yaml without setting actual values. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add module-level placement support Add placement constraints at the module level in module.yaml that apply to all definitions unless overridden at definition level. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add CLI placement enforcement in apply-module Add placement constraint checking to `vela def apply-module` command. Definitions are skipped if cluster labels don't match module placement. - Add --ignore-placement flag to bypass placement checks - Display placement status during apply with clear skip reasons - Track placement-skipped count in summary output Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): show all flags in subcommand help output Fix custom help function to properly display flags for def subcommands like init-module and apply-module instead of only showing parent flags. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): apply name prefix to definitions in apply-module The --prefix flag was not being applied to definition names. The prefix was set in module loader metadata but not used when creating Kubernetes objects from parsed CUE. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore(defkit): align module command help with standard vela pattern Remove argument placeholders from command Use field to align with other vela commands (addon, cluster, workflow). Arguments are shown in examples and individual --help output instead of the listing. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(goloader): use json.Unmarshal for go mod download output The downloadGoModule function parses JSON output from 'go mod download -json' but was incorrectly using yaml.Unmarshal with json struct tags. The yaml.v3 library ignores json tags, resulting in empty field values. This would cause remote Go module loading (e.g., github.com/foo/bar@v1.0.0) to fail with "go mod download did not return a directory" because result.Dir would be empty. Fix: Use json.Unmarshal instead since the data is JSON from the Go toolchain. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(goloader): use semver for MinVelaVersion comparison String comparison of version numbers is incorrect for cases like "v1.10.0" > "v1.9.0" which returns false due to lexicographic ordering. Use the Masterminds/semver library (already a dependency) for proper semantic version comparison in ValidateModule(). Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(placement): validate operator in module placement conditions Add validation to catch invalid placement operators at module load time instead of silently failing at runtime evaluation. - Add Operator.IsValid() method to check for valid operators - Add ValidOperators() helper function - Add validatePlacementConditions() in ValidateModule() - Provides clear error message with valid operator list Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(cli): validate conflict strategy in apply-module Invalid --conflict values like "invalid" were silently accepted and would fall through the switch statement, behaving like "overwrite". Add ConflictStrategy.IsValid() method and validation at flag parsing to provide clear error message for invalid values. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(placement): support definition-level placement constraints Previously only module-level placement was enforced. Now individual definitions can specify their own placement constraints that override module defaults. Changes: - Add Placement field to DefinitionInfo and DefinitionPlacement types - Add GetPlacement/HasPlacement to Definition interface - Update registry ToJSON to include placement in output - Update goloader to capture definition placement from registry - Update CLI apply-module to use GetEffectivePlacement() for combining module-level and definition-level placement - Add comprehensive tests for definition placement Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore(defkit): remove dead PatchTemplate code PatchTemplate, PatchOp, SetPatchOp, and SetIfPatchOp were defined but never used anywhere in the codebase. The PatchResource type already provides the same functionality and is the one actually being used through Template.Patch(). Removed: - PatchTemplate struct and its methods (ToCue, SetIf, Set) - PatchOp interface - SetPatchOp struct and its ToCue method - SetIfPatchOp struct and its ToCue method - NewPatchTemplate constructor This cleanup reduces maintenance burden without affecting any functionality. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(cli): pass actual VelaVersion to validate-module command The help text for `vela def validate-module` promised to check minVelaVersion requirements but ValidateModule() was called with an empty string, causing the check to be silently skipped. Now passes velaversion.VelaVersion so modules specifying a minimum KubeVela version will be properly validated against the current CLI version. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): implement WithDetails() and FromTyped() APIs WithDetails(): - Adds WithDetails(message, details...) method to StatusBuilder - Allows adding structured key-value details alongside status messages - Uses existing StatusDetail and statusWithDetailsExpr infrastructure - Example: s.WithDetails(s.Format("Ready: %v", ...), s.Detail("endpoint", ...)) FromTyped(): - Converts typed Kubernetes objects (runtime.Object) to Resource - Provides compile-time type safety for building resources - Requires TypeMeta to be set on the object - Includes MustFromTyped() variant that panics on error - Example: defkit.FromTyped(&appsv1.Deployment{...}) Both APIs were documented in the KEP but not implemented. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Style(defkit): apply gofmt formatting Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): fix remote module download with @latest version When downloading a Go module without an explicit version, always append @latest to ensure go mod download fetches from the remote repository instead of skipping the download. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): support running def commands from any directory Previously, module commands like `vela def list-module` only worked when run from within the kubevela repository. Now they work from any directory by honoring replace directives in the source module's go.mod. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): generate doc.go files in init-module Create doc.go files with package documentation in each definition directory (components, traits, policies, workflowsteps). This ensures go mod tidy works correctly by making each directory a valid Go package, and provides helpful examples for users creating new definitions. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): deduplicate definitions from overlapping directory scans The module loader scans both conventional directories (components/, traits/, etc.) and the root directory. Since DiscoverDefinitions uses recursive filepath.Walk, files in subdirectories were found twice. Added file tracking to skip already-processed files. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(defkit): validate placement constraints and fix GOWORK interference Add validation for conflicting placement constraints at registration time. Definitions with logically impossible placement (e.g., same condition in both RunOn and NotRunOn) now fail fast with a clear error message. Also fix placement loading when parent directories contain go.work files by setting GOWORK=off when running the registry generator. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add parameter schema constraints and runtime condition methods Extend the parameter fluent API with comprehensive validation and conditional logic support: - Schema constraints for input validation (Min/Max, Pattern, MinLen/MaxLen, MinItems/MaxItems) - Runtime conditions for template logic (In, Contains, Matches, StartsWith/EndsWith, Len*, IsEmpty/IsNotEmpty, HasKey, IsFalse) Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(defkit): add waitFor support with CUE expressions for module hooks Add the ability to specify custom readiness conditions for module hooks using the new `waitFor` field. This allows users to define precise conditions for when resources should be considered ready. The waitFor field supports two formats: - Simple condition name (e.g., "Ready", "Established") - checks status.conditions for the named condition with status "True" - CUE expression (e.g., "status.replicas == status.readyReplicas") - evaluated against the full resource for flexible readiness checks Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Feat(addon): add godef support for Go-based definitions in addons Add support for a godef/ folder in addons that allows writing definitions in Go instead of CUE. When an addon is enabled, Go definitions are automatically compiled to CUE and deployed alongside traditional CUE definitions. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: lint issues and make reviewable Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: lint and build failure Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: lint and ci errors Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: golangci-lint errors for defkit package - Use standard library errors (errors.Is/As) instead of pkg/errors - Fix ineffassign issues by scoping variables correctly - Add nolint comments for intentional nilerr, makezero patterns - Combine chained appends in addon init.go - Add gosec nolint for CLI file operations and permissions - Increase gocyclo threshold to 35, nolint complex CLI commands Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: kubectl installation with retry and fallback version in github actions Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix(ci): hardcode kubectl version to avoid flaky CDN endpoint Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore: improve test coverage for codecov Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Chore: add more tests for codecov and CI to pass Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: ci failure on style Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: OperatorNotEquals to fail closed with empty values Change NotEquals operator to return false when Values slice is empty, matching the fail-closed behavior of Equals operator. This prevents silent widening of placement eligibility when a malformed constraint is created. Following Kubernetes label selector semantics where In/NotIn operators require non-empty values, we apply a fail-closed approach for safety in placement decisions. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: OpenArrayParam field shadowing and remove redundant GetName() Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: path traversal vulnerability in Go definition scaffolding Validate Go definition names before using them in file paths to prevent creation of files outside the addon directory. Unsanitized names could contain path traversal segments (e.g., "../../../etc/passwd") allowing arbitrary file writes. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: unescaped string interpolation in health_expr CUE generation Use %q format verb in formatValue() to properly escape quotes and special characters when generating CUE strings. Update fieldContainsExpr to use formatValue() instead of raw string interpolation. This prevents invalid CUE when substring values contain quotes or backslashes. Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: Guard against typed nil in Gomega matchers to prevent panic Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: Guard against malformed bracket path in parseBracketAccess Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: incomplete AppRevision test to actually verify resolution Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Fix: apply fail-closed behavior to NotIn with empty values Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> * Doc: Added note about RawCUE and some alignment style Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in> --------- Signed-off-by: Anoop Gopalakrishnan <anoop2811@aol.in>
1937 lines
61 KiB
Go
1937 lines
61 KiB
Go
/*
|
|
Copyright 2021 The KubeVela 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 addon
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"cuelang.org/go/cue"
|
|
"cuelang.org/go/cue/cuecontext"
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/google/go-github/v32/github"
|
|
"github.com/imdario/mergo"
|
|
"github.com/pkg/errors"
|
|
gitlab "gitlab.com/gitlab-org/api/client-go"
|
|
"go.uber.org/multierr"
|
|
"golang.org/x/oauth2"
|
|
"helm.sh/helm/v3/pkg/chart/loader"
|
|
"helm.sh/helm/v3/pkg/chartutil"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
|
types2 "k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/client-go/discovery"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/util/retry"
|
|
"k8s.io/klog/v2"
|
|
stringslices "k8s.io/utils/strings/slices"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
"github.com/oam-dev/kubevela/pkg/config"
|
|
"github.com/oam-dev/kubevela/pkg/definition"
|
|
"github.com/oam-dev/kubevela/pkg/multicluster"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
"github.com/oam-dev/kubevela/pkg/oam/util"
|
|
"github.com/oam-dev/kubevela/pkg/schema"
|
|
"github.com/oam-dev/kubevela/pkg/utils"
|
|
addonutil "github.com/oam-dev/kubevela/pkg/utils/addon"
|
|
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
|
"github.com/oam-dev/kubevela/pkg/utils/common"
|
|
"github.com/oam-dev/kubevela/pkg/velaql"
|
|
version2 "github.com/oam-dev/kubevela/version"
|
|
)
|
|
|
|
const (
|
|
// ReadmeFileName is the addon readme file name
|
|
ReadmeFileName string = "README.md"
|
|
|
|
// LegacyReadmeFileName is the addon readme lower case file name
|
|
LegacyReadmeFileName string = "readme.md"
|
|
|
|
// MetadataFileName is the addon meatadata.yaml file name
|
|
MetadataFileName string = "metadata.yaml"
|
|
|
|
// TemplateFileName is the addon template.yaml file name
|
|
TemplateFileName string = "template.yaml"
|
|
|
|
// AppTemplateCueFileName is the addon application template.cue file name
|
|
AppTemplateCueFileName string = "template.cue"
|
|
|
|
// NotesCUEFileName is the addon notes print to end users when installed
|
|
NotesCUEFileName string = "NOTES.cue"
|
|
|
|
// KeyWordNotes is the keyword in NOTES.cue which will render the notes message out.
|
|
KeyWordNotes string = "notes"
|
|
|
|
// GlobalParameterFileName is the addon global parameter.cue file name
|
|
GlobalParameterFileName string = "parameter.cue"
|
|
|
|
// ResourcesDirName is the addon resources/ dir name
|
|
ResourcesDirName string = "resources"
|
|
|
|
// DefinitionsDirName is the addon definitions/ dir name
|
|
DefinitionsDirName string = "definitions"
|
|
|
|
// ConfigTemplateDirName is the addon config-templates/ dir name
|
|
ConfigTemplateDirName string = "config-templates"
|
|
|
|
// DefSchemaName is the addon definition schemas dir name
|
|
DefSchemaName string = "schemas"
|
|
|
|
// ViewDirName is the addon views dir name
|
|
ViewDirName string = "views"
|
|
|
|
// GoDefDirName is the addon godef/ dir name for Go-based definitions
|
|
GoDefDirName string = "godef"
|
|
|
|
// GoDefModuleFileName is the module.yaml file name in godef/
|
|
GoDefModuleFileName string = "module.yaml"
|
|
|
|
// AddonParameterDataKey is the key of parameter in addon args secrets
|
|
AddonParameterDataKey string = "addonParameterDataKey"
|
|
|
|
// DefaultGiteeURL is the addon repository of gitee api
|
|
DefaultGiteeURL string = "https://gitee.com/api/v5/"
|
|
|
|
// InstallerRuntimeOption inject install runtime info into addon options
|
|
InstallerRuntimeOption string = "installerRuntimeOption"
|
|
|
|
// CUEExtension with the expected extension for CUE files
|
|
CUEExtension = ".cue"
|
|
)
|
|
|
|
// ParameterFileName is the addon resources/parameter.cue file name
|
|
var ParameterFileName = strings.Join([]string{"resources", "parameter.cue"}, "/")
|
|
|
|
// ListOptions contains flags mark what files should be read in an addon directory
|
|
type ListOptions struct {
|
|
GetDetail bool
|
|
GetDefinition bool
|
|
GetConfigTemplate bool
|
|
GetResource bool
|
|
GetParameter bool
|
|
GetTemplate bool
|
|
GetDefSchema bool
|
|
}
|
|
|
|
var (
|
|
// UIMetaOptions get Addon metadata for UI display
|
|
UIMetaOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true, GetConfigTemplate: true}
|
|
|
|
// CLIMetaOptions get Addon metadata for CLI display
|
|
CLIMetaOptions = ListOptions{}
|
|
|
|
// UnInstallOptions used for addon uninstalling
|
|
UnInstallOptions = ListOptions{GetDefinition: true}
|
|
)
|
|
|
|
const (
|
|
// LocalAddonRegistryName is the addon-registry name for those installed by local dir
|
|
LocalAddonRegistryName = "local"
|
|
// ClusterLabelSelector define the key of topology cluster label selector
|
|
ClusterLabelSelector = "clusterLabelSelector"
|
|
)
|
|
|
|
// Pattern indicates the addon framework file pattern, all files should match at least one of the pattern.
|
|
type Pattern struct {
|
|
IsDir bool
|
|
Value string
|
|
}
|
|
|
|
// Patterns is the file pattern that the addon should be in
|
|
var Patterns = []Pattern{
|
|
// config-templates pattern
|
|
{IsDir: true, Value: ConfigTemplateDirName},
|
|
// single file reader pattern
|
|
{Value: ReadmeFileName}, {Value: MetadataFileName}, {Value: TemplateFileName},
|
|
// parameter in resource directory
|
|
{Value: ParameterFileName},
|
|
// directory files
|
|
{IsDir: true, Value: ResourcesDirName}, {IsDir: true, Value: DefinitionsDirName}, {IsDir: true, Value: DefSchemaName}, {IsDir: true, Value: ViewDirName},
|
|
// Go-based definitions directory
|
|
{IsDir: true, Value: GoDefDirName},
|
|
// CUE app template, parameter and notes
|
|
{Value: AppTemplateCueFileName}, {Value: GlobalParameterFileName}, {Value: NotesCUEFileName},
|
|
{Value: LegacyReadmeFileName}}
|
|
|
|
// GetPatternFromItem will check if the file path has a valid pattern, return empty string if it's invalid.
|
|
// AsyncReader is needed to calculate relative path
|
|
func GetPatternFromItem(it Item, r AsyncReader, rootPath string) string {
|
|
relativePath := r.RelativePath(it)
|
|
for _, p := range Patterns {
|
|
if strings.HasPrefix(relativePath, strings.Join([]string{rootPath, p.Value}, "/")) {
|
|
return p.Value
|
|
}
|
|
if strings.HasPrefix(relativePath, filepath.Join(rootPath, p.Value)) {
|
|
// for enable addon by load dir, compatible with linux or windows os
|
|
return p.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ListAddonUIDataFromReader list addons from AsyncReader
|
|
func ListAddonUIDataFromReader(r AsyncReader, registryMeta map[string]SourceMeta, registryName string, opt ListOptions) ([]*UIData, error) {
|
|
var addons []*UIData
|
|
var err error
|
|
var wg sync.WaitGroup
|
|
var errs []error
|
|
errCh := make(chan error)
|
|
waitCh := make(chan struct{})
|
|
|
|
var l sync.Mutex
|
|
for _, subItem := range registryMeta {
|
|
wg.Add(1)
|
|
go func(addonMeta SourceMeta) {
|
|
defer wg.Done()
|
|
addonRes, err := GetUIDataFromReader(r, &addonMeta, opt)
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
addonRes.RegistryName = registryName
|
|
l.Lock()
|
|
addons = append(addons, addonRes)
|
|
l.Unlock()
|
|
}(subItem)
|
|
}
|
|
// in another goroutine for wait group to finish
|
|
go func() {
|
|
wg.Wait()
|
|
close(waitCh)
|
|
}()
|
|
forLoop:
|
|
for {
|
|
select {
|
|
case <-waitCh:
|
|
break forLoop
|
|
case err = <-errCh:
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if len(errs) != 0 {
|
|
return addons, compactErrors("error(s) happen when reading from registry: ", errs)
|
|
}
|
|
return addons, nil
|
|
}
|
|
|
|
func compactErrors(message string, errs []error) error {
|
|
errForPrint := make([]string, 0)
|
|
for _, e := range errs {
|
|
errForPrint = append(errForPrint, e.Error())
|
|
}
|
|
|
|
return errors.New(message + strings.Join(errForPrint, ","))
|
|
|
|
}
|
|
|
|
// GetUIDataFromReader read ui metadata of addon from Reader, used to be displayed in UI
|
|
func GetUIDataFromReader(r AsyncReader, meta *SourceMeta, opt ListOptions) (*UIData, error) {
|
|
addonContentsReader := map[string]struct {
|
|
skip bool
|
|
read func(a *UIData, reader AsyncReader, readPath string) error
|
|
}{
|
|
ReadmeFileName: {!opt.GetDetail, readReadme},
|
|
LegacyReadmeFileName: {!opt.GetDetail, readReadme},
|
|
MetadataFileName: {false, readMetadata},
|
|
DefinitionsDirName: {!opt.GetDefinition, readDefFile},
|
|
ConfigTemplateDirName: {!opt.GetConfigTemplate, readConfigTemplateFile},
|
|
ParameterFileName: {!opt.GetParameter, readParamFile},
|
|
GlobalParameterFileName: {!opt.GetParameter, readGlobalParamFile},
|
|
}
|
|
ptItems := ClassifyItemByPattern(meta, r)
|
|
var addon = &UIData{}
|
|
for contentType, method := range addonContentsReader {
|
|
if method.skip {
|
|
continue
|
|
}
|
|
items := ptItems[contentType]
|
|
for _, it := range items {
|
|
err := method.read(addon, r, r.RelativePath(it))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fail to read addon %s file %s: %w", meta.Name, r.RelativePath(it), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if opt.GetParameter && (len(addon.Parameters) != 0 || len(addon.GlobalParameters) != 0) {
|
|
if addon.GlobalParameters != "" {
|
|
if addon.Parameters != "" {
|
|
klog.Warning("both legacy parameter and global parameter are provided, but only global parameter will be used. Consider removing the legacy parameters.")
|
|
}
|
|
addon.Parameters = addon.GlobalParameters
|
|
}
|
|
err := genAddonAPISchema(addon)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fail to generate openAPIschema for addon %s : %w (parameter: %s)", meta.Name, err, addon.Parameters)
|
|
}
|
|
}
|
|
addon.AvailableVersions = []string{addon.Version}
|
|
return addon, nil
|
|
}
|
|
|
|
// GetInstallPackageFromReader get install package of addon from Reader, this is used to enable an addon
|
|
func GetInstallPackageFromReader(r AsyncReader, meta *SourceMeta, uiData *UIData) (*InstallPackage, error) {
|
|
addonContentsReader := map[string]func(a *InstallPackage, reader AsyncReader, readPath string) error{
|
|
TemplateFileName: readTemplate,
|
|
ResourcesDirName: readResFile,
|
|
DefSchemaName: readDefSchemaFile,
|
|
ViewDirName: readViewFile,
|
|
AppTemplateCueFileName: readAppCueTemplate,
|
|
NotesCUEFileName: readNotesFile,
|
|
}
|
|
ptItems := ClassifyItemByPattern(meta, r)
|
|
|
|
// Read the installed data from UI metadata object to reduce network payload
|
|
var addon = &InstallPackage{
|
|
Meta: uiData.Meta,
|
|
Definitions: uiData.Definitions,
|
|
CUEDefinitions: uiData.CUEDefinitions,
|
|
Parameters: uiData.Parameters,
|
|
ConfigTemplates: uiData.ConfigTemplates,
|
|
}
|
|
|
|
for contentType, method := range addonContentsReader {
|
|
items := ptItems[contentType]
|
|
for _, it := range items {
|
|
err := method(addon, r, r.RelativePath(it))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fail to read addon %s file %s: %w", meta.Name, r.RelativePath(it), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return addon, nil
|
|
}
|
|
|
|
func readTemplate(a *InstallPackage, reader AsyncReader, readPath string) error {
|
|
data, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
|
a.AppTemplate = &v1beta1.Application{}
|
|
|
|
// try to check it's a valid app template
|
|
_, _, err = dec.Decode([]byte(data), nil, a.AppTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readAppCueTemplate(a *InstallPackage, reader AsyncReader, readPath string) error {
|
|
data, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.AppCueTemplate = ElementFile{Data: data, Name: filepath.Base(readPath)}
|
|
return nil
|
|
}
|
|
|
|
// readParamFile read single resource/parameter.cue file
|
|
func readParamFile(a *UIData, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.Parameters = b
|
|
return nil
|
|
}
|
|
|
|
// readNotesFile read single NOTES.cue file
|
|
func readNotesFile(a *InstallPackage, reader AsyncReader, readPath string) error {
|
|
data, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.Notes = ElementFile{Data: data, Name: filepath.Base(readPath)}
|
|
return nil
|
|
}
|
|
|
|
// readGlobalParamFile read global parameter file.
|
|
func readGlobalParamFile(a *UIData, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.GlobalParameters = b
|
|
return nil
|
|
}
|
|
|
|
// readResFile read single resource file
|
|
func readResFile(a *InstallPackage, reader AsyncReader, readPath string) error {
|
|
filename := path.Base(readPath)
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if filename == "parameter.cue" {
|
|
return nil
|
|
}
|
|
file := ElementFile{Data: b, Name: filepath.Base(readPath)}
|
|
switch filepath.Ext(filename) {
|
|
case CUEExtension:
|
|
a.CUETemplates = append(a.CUETemplates, file)
|
|
case ".yaml", ".yml":
|
|
a.YAMLTemplates = append(a.YAMLTemplates, file)
|
|
default:
|
|
// skip other file formats
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readDefSchemaFile read single file of definition schema
|
|
func readDefSchemaFile(a *InstallPackage, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.DefSchemas = append(a.DefSchemas, ElementFile{Data: b, Name: filepath.Base(readPath)})
|
|
return nil
|
|
}
|
|
|
|
// readDefFile read single definition file
|
|
func readDefFile(a *UIData, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filename := path.Base(readPath)
|
|
file := ElementFile{Data: b, Name: filepath.Base(readPath)}
|
|
switch filepath.Ext(filename) {
|
|
case CUEExtension:
|
|
a.CUEDefinitions = append(a.CUEDefinitions, file)
|
|
case ".yaml", ".yml":
|
|
a.Definitions = append(a.Definitions, file)
|
|
default:
|
|
// skip other file formats
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// readConfigTemplateFile read single template file of the config
|
|
func readConfigTemplateFile(a *UIData, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filename := path.Base(readPath)
|
|
if filepath.Ext(filename) != CUEExtension {
|
|
return nil
|
|
}
|
|
file := ElementFile{Data: b, Name: filepath.Base(readPath)}
|
|
a.ConfigTemplates = append(a.ConfigTemplates, file)
|
|
return nil
|
|
}
|
|
|
|
// readViewFile read single view file
|
|
func readViewFile(a *InstallPackage, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filename := path.Base(readPath)
|
|
switch filepath.Ext(filename) {
|
|
case CUEExtension:
|
|
a.CUEViews = append(a.CUEViews, ElementFile{Data: b, Name: filepath.Base(readPath)})
|
|
case ".yaml", ".yml":
|
|
a.YAMLViews = append(a.YAMLViews, ElementFile{Data: b, Name: filepath.Base(readPath)})
|
|
default:
|
|
// skip other file formats
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readMetadata(a *UIData, reader AsyncReader, readPath string) error {
|
|
b, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = yaml.Unmarshal([]byte(b), &a.Meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readReadme(a *UIData, reader AsyncReader, readPath string) error {
|
|
// the detail will contain readme.md or README.md, if the content already is filled, don't read another.
|
|
if len(a.Detail) != 0 {
|
|
return nil
|
|
}
|
|
content, err := reader.ReadFile(readPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.Detail = content
|
|
return nil
|
|
}
|
|
|
|
func createGitHelper(content *utils.Content, token string) *gitHelper {
|
|
var ts oauth2.TokenSource
|
|
if token != "" {
|
|
ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
|
}
|
|
tc := oauth2.NewClient(context.Background(), ts)
|
|
tc.Timeout = time.Second * 20
|
|
cli := github.NewClient(tc)
|
|
return &gitHelper{
|
|
Client: cli,
|
|
Meta: content,
|
|
}
|
|
}
|
|
|
|
func createGiteeHelper(content *utils.Content, token string) *giteeHelper {
|
|
var ts oauth2.TokenSource
|
|
if token != "" {
|
|
ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
|
}
|
|
tc := oauth2.NewClient(context.Background(), ts)
|
|
tc.Timeout = time.Second * 20
|
|
cli := NewGiteeClient(tc, nil)
|
|
return &giteeHelper{
|
|
Client: cli,
|
|
Meta: content,
|
|
}
|
|
}
|
|
|
|
func createGitlabHelper(content *utils.Content, token string) (*gitlabHelper, error) {
|
|
newClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(content.GitlabContent.Host))
|
|
|
|
return &gitlabHelper{
|
|
Client: newClient,
|
|
Meta: content,
|
|
}, err
|
|
}
|
|
|
|
// readRepo will read relative path (relative to Meta.Path)
|
|
func (h *gitHelper) readRepo(relativePath string) (*github.RepositoryContent, []*github.RepositoryContent, error) {
|
|
file, items, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.GithubContent.Owner, h.Meta.GithubContent.Repo, path.Join(h.Meta.GithubContent.Path, relativePath), nil)
|
|
if err != nil {
|
|
return nil, nil, WrapErrRateLimit(err)
|
|
}
|
|
return file, items, nil
|
|
}
|
|
|
|
// readRepo will read relative path (relative to Meta.Path)
|
|
func (h *giteeHelper) readRepo(relativePath string) (*github.RepositoryContent, []*github.RepositoryContent, error) {
|
|
file, items, err := h.Client.GetGiteeContents(context.Background(), h.Meta.GiteeContent.Owner, h.Meta.GiteeContent.Repo, path.Join(h.Meta.GiteeContent.Path, relativePath), h.Meta.GiteeContent.Ref)
|
|
if err != nil {
|
|
return nil, nil, WrapErrRateLimit(err)
|
|
}
|
|
return file, items, nil
|
|
}
|
|
|
|
// GetGiteeContents can return either the metadata and content of a single file
|
|
func (c *Client) GetGiteeContents(ctx context.Context, owner, repo, path, ref string) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, err error) {
|
|
escapedPath := (&url.URL{Path: path}).String()
|
|
u := fmt.Sprintf(c.BaseURL.String()+"repos/%s/%s/contents/%s", owner, repo, escapedPath)
|
|
if ref != "" {
|
|
u = fmt.Sprintf(u+"?ref=%s", ref)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
response, err := c.Client.Do(req.WithContext(ctx))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
//nolint:errcheck
|
|
defer response.Body.Close()
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return unmarshalToContent(body)
|
|
}
|
|
|
|
func unmarshalToContent(content []byte) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, err error) {
|
|
fileUnmarshalError := json.Unmarshal(content, &fileContent)
|
|
if fileUnmarshalError == nil {
|
|
return fileContent, nil, nil
|
|
}
|
|
directoryUnmarshalError := json.Unmarshal(content, &directoryContent)
|
|
if directoryUnmarshalError == nil {
|
|
return nil, directoryContent, nil
|
|
}
|
|
return nil, nil, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %w", fileUnmarshalError.Error(), directoryUnmarshalError)
|
|
}
|
|
|
|
func genAddonAPISchema(addonRes *UIData) error {
|
|
s, err := schema.ParsePropertiesToSchema(context.Background(), addonRes.Parameters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addonRes.APISchema = s
|
|
return nil
|
|
}
|
|
|
|
func getClusters(args map[string]interface{}) []string {
|
|
ccr, ok := args[types.ClustersArg]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cc, ok := ccr.([]string)
|
|
if ok {
|
|
return cc
|
|
}
|
|
ccrslice, ok := ccr.([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var ccstring []string
|
|
for _, c := range ccrslice {
|
|
if cstring, ok := c.(string); ok {
|
|
ccstring = append(ccstring, cstring)
|
|
}
|
|
}
|
|
return ccstring
|
|
}
|
|
|
|
// renderNeededNamespaceAsComps will convert namespace as app components to create namespace for managed clusters
|
|
func renderNeededNamespaceAsComps(addon *InstallPackage) []common2.ApplicationComponent {
|
|
var nscomps []common2.ApplicationComponent
|
|
// create namespace for managed clusters
|
|
for _, namespace := range addon.NeedNamespace {
|
|
// vela-system must exist before rendering vela addon
|
|
if namespace == types.DefaultKubeVelaNS {
|
|
continue
|
|
}
|
|
comp := common2.ApplicationComponent{
|
|
Type: "raw",
|
|
Name: fmt.Sprintf("%s-namespace", namespace),
|
|
Properties: util.Object2RawExtension(renderNamespace(namespace)),
|
|
}
|
|
nscomps = append(nscomps, comp)
|
|
}
|
|
return nscomps
|
|
}
|
|
|
|
func checkDeployClusters(ctx context.Context, k8sClient client.Client, args map[string]interface{}) ([]string, error) {
|
|
deployClusters := getClusters(args)
|
|
if len(deployClusters) == 0 || k8sClient == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
clusters, err := multicluster.NewClusterClient(k8sClient).List(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fail to get registered cluster")
|
|
}
|
|
|
|
clusterNames := sets.Set[string]{}
|
|
if len(clusters.Items) != 0 {
|
|
for _, cluster := range clusters.Items {
|
|
clusterNames.Insert(cluster.Name)
|
|
}
|
|
}
|
|
|
|
var res []string
|
|
for _, c := range deployClusters {
|
|
c = strings.TrimSpace(c)
|
|
if c == "" {
|
|
continue
|
|
}
|
|
if !clusterNames.Has(c) {
|
|
return nil, errors.Errorf("cluster %s not exist", c)
|
|
}
|
|
res = append(res, c)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// RenderDefinitions render definition objects if needed
|
|
func RenderDefinitions(addon *InstallPackage, config *rest.Config) ([]*unstructured.Unstructured, error) {
|
|
defObjs := make([]*unstructured.Unstructured, 0)
|
|
|
|
// No matter runtime mode or control mode, definition only needs to control plane k8s.
|
|
for _, def := range addon.Definitions {
|
|
obj, err := renderObject(def)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "render definition file %s", def.Name)
|
|
}
|
|
// we should ignore the namespace defined in definition yaml, override the filed by DefaultKubeVelaNS
|
|
obj.SetNamespace(types.DefaultKubeVelaNS)
|
|
defObjs = append(defObjs, obj)
|
|
}
|
|
for _, cueDef := range addon.CUEDefinitions {
|
|
def := definition.Definition{Unstructured: unstructured.Unstructured{}}
|
|
err := def.FromCUEString(cueDef.Data, config)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "fail to render definition: %s in cue's format", cueDef.Name)
|
|
}
|
|
// we should ignore the namespace defined in definition yaml, override the filed by DefaultKubeVelaNS
|
|
def.SetNamespace(types.DefaultKubeVelaNS)
|
|
defObjs = append(defObjs, &def.Unstructured)
|
|
}
|
|
|
|
return defObjs, nil
|
|
}
|
|
|
|
// RenderConfigTemplates render the config template
|
|
func RenderConfigTemplates(ctx context.Context, addon *InstallPackage, cli client.Client) ([]*unstructured.Unstructured, error) {
|
|
templates := make([]*unstructured.Unstructured, 0)
|
|
|
|
factory := config.NewConfigFactory(cli)
|
|
for _, templateFile := range addon.ConfigTemplates {
|
|
t, err := factory.ParseTemplate(ctx, "", []byte(templateFile.Data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.ConfigMap.Namespace = types.DefaultKubeVelaNS
|
|
obj, err := util.Object2Unstructured(t.ConfigMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
obj.SetKind("ConfigMap")
|
|
obj.SetAPIVersion("v1")
|
|
templates = append(templates, obj)
|
|
}
|
|
|
|
return templates, nil
|
|
}
|
|
|
|
// RenderDefinitionSchema will render definitions' schema in addons.
|
|
func RenderDefinitionSchema(addon *InstallPackage) ([]*unstructured.Unstructured, error) {
|
|
schemaConfigmaps := make([]*unstructured.Unstructured, 0)
|
|
|
|
// No matter runtime mode or control mode , definition schemas only needs to control plane k8s.
|
|
for _, teml := range addon.DefSchemas {
|
|
u, err := renderSchemaConfigmap(teml)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "render uiSchema file %s", teml.Name)
|
|
}
|
|
schemaConfigmaps = append(schemaConfigmaps, u)
|
|
}
|
|
return schemaConfigmaps, nil
|
|
}
|
|
|
|
// RenderViews will render views in addons.
|
|
func RenderViews(ctx context.Context, addon *InstallPackage) ([]*unstructured.Unstructured, error) {
|
|
views := make([]*unstructured.Unstructured, 0)
|
|
for _, view := range addon.YAMLViews {
|
|
obj, err := renderObject(view)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "render velaQL view file %s", view.Name)
|
|
}
|
|
views = append(views, obj)
|
|
}
|
|
for _, view := range addon.CUEViews {
|
|
obj, err := renderCUEView(ctx, view)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "render velaQL view file %s", view.Name)
|
|
}
|
|
views = append(views, obj)
|
|
}
|
|
return views, nil
|
|
}
|
|
|
|
func renderObject(elem ElementFile) (*unstructured.Unstructured, error) {
|
|
obj := &unstructured.Unstructured{}
|
|
dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
|
_, _, err := dec.Decode([]byte(elem.Data), nil, obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
func renderNamespace(namespace string) *unstructured.Unstructured {
|
|
u := &unstructured.Unstructured{}
|
|
u.SetAPIVersion("v1")
|
|
u.SetKind("Namespace")
|
|
u.SetName(namespace)
|
|
return u
|
|
}
|
|
|
|
func renderK8sObjectsComponent(elems []ElementFile, addonName string) (*common2.ApplicationComponent, error) {
|
|
var objects []*unstructured.Unstructured
|
|
for _, elem := range elems {
|
|
obj, err := renderObject(elem)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "render resource file %s", elem.Name)
|
|
}
|
|
objects = append(objects, obj)
|
|
}
|
|
properties := map[string]interface{}{"objects": objects}
|
|
propJSON, err := json.Marshal(properties)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseRawComponent := common2.ApplicationComponent{
|
|
Type: "k8s-objects",
|
|
Name: addonName + "-resources",
|
|
Properties: &runtime.RawExtension{Raw: propJSON},
|
|
}
|
|
return &baseRawComponent, nil
|
|
}
|
|
|
|
func renderSchemaConfigmap(elem ElementFile) (*unstructured.Unstructured, error) {
|
|
jsonData, err := yaml.YAMLToJSON([]byte(elem.Data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cm := v1.ConfigMap{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
|
|
ObjectMeta: metav1.ObjectMeta{Namespace: types.DefaultKubeVelaNS, Name: strings.Split(elem.Name, ".")[0]},
|
|
Data: map[string]string{
|
|
types.UISchema: string(jsonData),
|
|
}}
|
|
return util.Object2Unstructured(cm)
|
|
}
|
|
|
|
func renderCUEView(ctx context.Context, elem ElementFile) (*unstructured.Unstructured, error) {
|
|
name, err := utils.GetFilenameFromLocalOrRemote(elem.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cm, err := velaql.ParseViewIntoConfigMap(ctx, elem.Data, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return util.Object2Unstructured(*cm)
|
|
}
|
|
|
|
// RenderArgsSecret render addon enable argument to secret to remember when restart or upgrade
|
|
func RenderArgsSecret(addon *InstallPackage, args map[string]interface{}) *unstructured.Unstructured {
|
|
argsByte, err := json.Marshal(args)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
sec := v1.Secret{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: addonutil.Addon2SecName(addon.Name),
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
},
|
|
Data: map[string][]byte{
|
|
AddonParameterDataKey: argsByte,
|
|
},
|
|
Type: v1.SecretTypeOpaque,
|
|
}
|
|
u, err := util.Object2Unstructured(sec)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return u
|
|
}
|
|
|
|
// deleteArgsSecret delete the addon's args secret file
|
|
func deleteArgsSecret(ctx context.Context, k8sClient client.Client, addonName string) error {
|
|
var sec v1.Secret
|
|
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(addonName)}, &sec); err == nil {
|
|
// Handle successful get operation
|
|
return k8sClient.Delete(ctx, &sec)
|
|
} else if !apierrors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FetchArgsFromSecret fetch addon args from secrets
|
|
func FetchArgsFromSecret(sec *v1.Secret) (map[string]interface{}, error) {
|
|
res := map[string]interface{}{}
|
|
if args, ok := sec.Data[AddonParameterDataKey]; ok {
|
|
err := json.Unmarshal(args, &res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// this is backward compatibility code for old way to storage parameter
|
|
res = make(map[string]interface{}, len(sec.Data))
|
|
for k, v := range sec.Data {
|
|
res[k] = string(v)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// Installer helps addon enable, dependency-check, dispatch resources
|
|
type Installer struct {
|
|
ctx context.Context
|
|
config *rest.Config
|
|
addon *InstallPackage
|
|
cli client.Client
|
|
apply apply.Applicator
|
|
r *Registry
|
|
registryMeta map[string]SourceMeta
|
|
args map[string]interface{}
|
|
cache *Cache
|
|
dc *discovery.DiscoveryClient
|
|
skipVersionValidate bool
|
|
overrideDefs bool
|
|
|
|
dryRun bool
|
|
dryRunBuff *bytes.Buffer
|
|
|
|
installerRuntime map[string]interface{}
|
|
|
|
registries []Registry
|
|
}
|
|
|
|
// NewAddonInstaller will create an installer for addon
|
|
func NewAddonInstaller(ctx context.Context, cli client.Client, discoveryClient *discovery.DiscoveryClient, apply apply.Applicator, config *rest.Config, r *Registry, args map[string]interface{}, cache *Cache, registries []Registry, opts ...InstallOption) Installer {
|
|
if args == nil {
|
|
args = map[string]interface{}{}
|
|
}
|
|
i := Installer{
|
|
ctx: ctx,
|
|
config: config,
|
|
cli: cli,
|
|
apply: apply,
|
|
r: r,
|
|
args: args,
|
|
cache: cache,
|
|
dc: discoveryClient,
|
|
dryRunBuff: &bytes.Buffer{},
|
|
registries: registries,
|
|
}
|
|
ir := args[InstallerRuntimeOption]
|
|
if irr, ok := ir.(map[string]interface{}); ok {
|
|
i.installerRuntime = irr
|
|
} else {
|
|
i.installerRuntime = map[string]interface{}{}
|
|
}
|
|
// clean injected data from runtime option
|
|
delete(args, InstallerRuntimeOption)
|
|
|
|
for _, opt := range opts {
|
|
opt(&i)
|
|
}
|
|
return i
|
|
}
|
|
|
|
func (h *Installer) enableAddon(ctx context.Context, addon *InstallPackage) (string, error) {
|
|
var err error
|
|
h.addon = addon
|
|
if !h.skipVersionValidate {
|
|
err = checkAddonVersionMeetRequired(h.ctx, addon.SystemRequirements, h.cli, h.dc)
|
|
if err != nil {
|
|
version := h.getAddonVersionMeetSystemRequirement(addon.Name)
|
|
return "", VersionUnMatchError{addonName: addon.Name, err: err, userSelectedAddonVersion: addon.Version, availableVersion: version}
|
|
}
|
|
}
|
|
|
|
if err = h.installDependency(ctx, addon); err != nil {
|
|
return "", err
|
|
}
|
|
if err = h.dispatchAddonResource(ctx, addon); err != nil {
|
|
return "", err
|
|
}
|
|
// we shouldn't put continue func into dispatchAddonResource, because the re-apply app maybe already update app and
|
|
// the suspend will set with false automatically
|
|
if err := h.continueOrRestartWorkflow(); err != nil {
|
|
return "", err
|
|
}
|
|
additionalInfo, err := h.renderNotes(addon)
|
|
if err != nil {
|
|
klog.Warningf("fail to render notes for addon %s: %v\n", addon.Name, err)
|
|
// notes don't affect the installation, so just print warn logs instead of abort with errors
|
|
return "", nil
|
|
}
|
|
return additionalInfo, nil
|
|
}
|
|
|
|
func (h *Installer) loadInstallPackage(name, version string) (*InstallPackage, error) {
|
|
var installPackage *InstallPackage
|
|
var err error
|
|
if !IsVersionRegistry(*h.r) {
|
|
metas, err := h.getAddonMeta()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fail to get addon meta")
|
|
}
|
|
|
|
meta, ok := metas[name]
|
|
if !ok {
|
|
return nil, ErrNotExist
|
|
}
|
|
var uiData *UIData
|
|
uiData, err = h.cache.GetUIData(*h.r, name, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// enable this addon if it's invisible
|
|
installPackage, err = h.r.GetInstallPackage(&meta, uiData)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fail to find dependent addon in source repository")
|
|
}
|
|
} else {
|
|
versionedRegistry := BuildVersionedRegistry(h.r.Name, h.r.Helm.URL, &common.HTTPOption{
|
|
Username: h.r.Helm.Username,
|
|
Password: h.r.Helm.Password,
|
|
InsecureSkipTLS: h.r.Helm.InsecureSkipTLS,
|
|
})
|
|
installPackage, err = versionedRegistry.GetAddonInstallPackage(context.Background(), name, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return installPackage, nil
|
|
}
|
|
|
|
func (h *Installer) getAddonMeta() (map[string]SourceMeta, error) {
|
|
var err error
|
|
if h.registryMeta == nil {
|
|
if h.registryMeta, err = h.cache.ListAddonMeta(*h.r); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return h.registryMeta, nil
|
|
}
|
|
|
|
// installDependency checks if addon's dependency and install it
|
|
func (h *Installer) installDependency(ctx context.Context, addon *InstallPackage) error {
|
|
installedAddons, err := listInstalledAddons(h.ctx, h.cli)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var registries []ItemInfoLister
|
|
registries = append(registries, h.r)
|
|
for _, registry := range h.registries {
|
|
r := registry
|
|
registries = append(registries, &r)
|
|
}
|
|
availableAddons, err := listAvailableAddons(registries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = validateAddonDependencies(addon, installedAddons, availableAddons)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var dependencies []string
|
|
var addonClusters = getClusters(h.args)
|
|
for _, dep := range addon.Dependencies {
|
|
needInstallAddonDep, err := checkDependencyNeedInstall(h.ctx, h.cli, dep.Name, addonClusters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !needInstallAddonDep {
|
|
continue
|
|
}
|
|
|
|
dependencies = append(dependencies, dep.Name)
|
|
if h.dryRun {
|
|
continue
|
|
}
|
|
depHandler := *h
|
|
// reset dependency addon clusters parameter
|
|
depArgs, depArgsErr := getDependencyArgs(h.ctx, h.cli, dep.Name, addonClusters)
|
|
if depArgsErr != nil {
|
|
return depArgsErr
|
|
}
|
|
|
|
depHandler.args = depArgs
|
|
|
|
var depAddon *InstallPackage
|
|
depVersion, err := calculateDependencyVersionToInstall(*dep, installedAddons, availableAddons)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// try to install the dependent addon from the same registry with the current addon
|
|
depAddon, err = h.loadInstallPackage(dep.Name, depVersion)
|
|
if err == nil {
|
|
additionalInfo, err := depHandler.enableAddon(ctx, depAddon)
|
|
if err != nil {
|
|
return errors.Wrap(err, "fail to dispatch dependent addon resource")
|
|
}
|
|
if len(additionalInfo) > 0 {
|
|
klog.Infof("addon %s installed with additional info: %s\n", addon.Name, additionalInfo)
|
|
}
|
|
return nil
|
|
}
|
|
if !errors.Is(err, ErrNotExist) {
|
|
return err
|
|
}
|
|
for _, registry := range h.registries {
|
|
// try to install dependent addon from other registries
|
|
depHandler.r = &Registry{
|
|
Name: registry.Name, Helm: registry.Helm, OSS: registry.OSS, Git: registry.Git, Gitee: registry.Gitee, Gitlab: registry.Gitlab,
|
|
}
|
|
depAddon, err = depHandler.loadInstallPackage(dep.Name, depVersion)
|
|
if err == nil {
|
|
break
|
|
}
|
|
if errors.Is(err, ErrNotExist) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if err == nil {
|
|
additionalInfo, err := depHandler.enableAddon(ctx, depAddon)
|
|
if err != nil {
|
|
return errors.Wrap(err, "fail to dispatch dependent addon resource")
|
|
}
|
|
if len(additionalInfo) > 0 {
|
|
klog.Infof("addon %s installed with additional info: %s\n", addon.Name, additionalInfo)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("dependency addon: %s with version: %s cannot be found from all registries", dep.Name, depVersion)
|
|
}
|
|
if h.dryRun && len(dependencies) > 0 {
|
|
klog.Warningf("dry run addon won't install dependencies, please make sure your system has already installed these addons: %v", strings.Join(dependencies, ", "))
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateAddonDependencies checks if addon's dependencies can be satisfied.
|
|
// If dependency is installed, check if the version matches required version.
|
|
// If dependency is not installed, check available addons for dependency
|
|
// matching the required version.
|
|
// Return error if any dependency cannot be satisfied.
|
|
func validateAddonDependencies(addon *InstallPackage, installedAddons itemInfoMap, availableAddons itemInfoMap) error {
|
|
var merr error
|
|
for _, dep := range addon.Dependencies {
|
|
_, err := calculateDependencyVersionToInstall(*dep, installedAddons, availableAddons)
|
|
if err != nil {
|
|
merr = multierr.Append(merr, fmt.Errorf("addon %s has unresolvable dependency %s: %w", addon.Name, dep.Name, err))
|
|
}
|
|
}
|
|
return merr
|
|
}
|
|
|
|
// calculateDependencyVersionToInstall compares an addon's dependency to a list
|
|
// of installed and available addons and returns a version to install.
|
|
// If dependency is installed, return the installed version if it matches the
|
|
// required version.
|
|
// If dependency is not installed, return the latest available version that
|
|
// satisfies the dependency version.
|
|
// Return error if dependency version cannot be satisfied.
|
|
func calculateDependencyVersionToInstall(dependency Dependency, installedAddons itemInfoMap, availableAddons itemInfoMap) (string, error) {
|
|
if dependency.Name == "" {
|
|
return "", fmt.Errorf("dependency name cannot be empty")
|
|
}
|
|
|
|
// if dependency is installed, return the installed version if it matches
|
|
// the required version
|
|
if installedAddons != nil {
|
|
installedAddon, ok := installedAddons[dependency.Name]
|
|
if ok {
|
|
// versions length must be 1
|
|
if len(installedAddon.AvailableVersions) != 1 {
|
|
return "", errors.New("installedAddon.Versions length must be 1")
|
|
}
|
|
installedVersion := installedAddon.AvailableVersions[0]
|
|
|
|
if dependency.Version == "" {
|
|
return installedVersion, nil
|
|
}
|
|
|
|
match, _ := checkSemVer(installedVersion, dependency.Version)
|
|
if match {
|
|
return installedVersion, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("addon %s version '%s' does not match installed version '%s'",
|
|
dependency.Name, dependency.Version, installedVersion)
|
|
}
|
|
}
|
|
|
|
availableAddon, ok := availableAddons[dependency.Name]
|
|
if !ok {
|
|
return "", fmt.Errorf("no available addon with name %s", dependency.Name)
|
|
}
|
|
|
|
sortedVersions := sortVersionsDescending(availableAddon.AvailableVersions)
|
|
|
|
// if no version is specified, return the latest version
|
|
if dependency.Version == "" {
|
|
return sortedVersions[0], nil
|
|
}
|
|
|
|
// check if the dependency version is satisfied
|
|
var match bool
|
|
for _, version := range sortedVersions {
|
|
match, _ = checkSemVer(version, dependency.Version)
|
|
if match {
|
|
return version, nil
|
|
}
|
|
}
|
|
|
|
// no available version satisfies the dependency version
|
|
return "", fmt.Errorf("no available addon with name %s and version '%s', available versions %s",
|
|
dependency.Name, dependency.Version, availableAddon.AvailableVersions)
|
|
}
|
|
|
|
func sortVersionsDescending(versions []string) []string {
|
|
var sortedVersions []*semver.Version
|
|
var sortedVersionStrings []string
|
|
for _, v := range versions {
|
|
var err error
|
|
// Note: NewVersion attempts to convert SemVer-ish formats into SemVer
|
|
parsedVersion, err := semver.NewVersion(v)
|
|
if err == nil {
|
|
sortedVersions = append(sortedVersions, parsedVersion)
|
|
}
|
|
}
|
|
// sort versions in descending order
|
|
sort.Sort(sort.Reverse(semver.Collection(sortedVersions)))
|
|
for _, v := range sortedVersions {
|
|
sortedVersionStrings = append(sortedVersionStrings, v.String())
|
|
}
|
|
return sortedVersionStrings
|
|
}
|
|
|
|
// ItemInfoLister is an interface for Registry.ListAddonInfo() to enable easier
|
|
// testing with mocks.
|
|
type ItemInfoLister interface {
|
|
ListAddonInfo() (map[string]ItemInfo, error)
|
|
}
|
|
|
|
// listAvailableAddons fetches a collection of addons available in a list of
|
|
// registries. Returns a map of ItemInfo grouped by addon name.
|
|
func listAvailableAddons(registries []ItemInfoLister) (itemInfoMap, error) {
|
|
availableAddons := make(itemInfoMap)
|
|
|
|
for _, registry := range registries {
|
|
addons, err := registry.ListAddonInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
availableAddons = mergeAddonInfoMaps(availableAddons, addons)
|
|
}
|
|
return availableAddons, nil
|
|
}
|
|
|
|
func mergeAddonInfoMaps(existingAddons itemInfoMap, newAddons itemInfoMap) itemInfoMap {
|
|
mergedAddons := existingAddons
|
|
for _, newAddon := range newAddons {
|
|
if existingAddon, ok := existingAddons[newAddon.Name]; ok {
|
|
// merge addon versions
|
|
existingVersions := existingAddon.AvailableVersions
|
|
newVersions := newAddon.AvailableVersions
|
|
|
|
mergedVersionsSet := make(map[string]bool)
|
|
|
|
for _, item := range existingVersions {
|
|
mergedVersionsSet[item] = true
|
|
}
|
|
for _, item := range newVersions {
|
|
mergedVersionsSet[item] = true
|
|
}
|
|
|
|
mergedVersions := make([]string, 0, len(mergedVersionsSet))
|
|
for item := range mergedVersionsSet {
|
|
mergedVersions = append(mergedVersions, item)
|
|
}
|
|
|
|
mergedVersions = sortVersionsDescending(mergedVersions)
|
|
|
|
existingAddon.AvailableVersions = mergedVersions
|
|
mergedAddons[existingAddon.Name] = existingAddon
|
|
} else {
|
|
mergedAddons[newAddon.Name] = newAddon
|
|
}
|
|
}
|
|
return mergedAddons
|
|
}
|
|
|
|
// listInstalledAddons fetches a collection of addons installed in the cluster.
|
|
// Returns a map of ItemInfo grouped by addon name.
|
|
func listInstalledAddons(ctx context.Context, k8sClient client.Client) (itemInfoMap, error) {
|
|
installedAddons := make(itemInfoMap)
|
|
// get all addons from cluster
|
|
// for each addon, get the version and add it to addonVersions
|
|
appList := &v1beta1.ApplicationList{}
|
|
err := k8sClient.List(ctx, appList, client.InNamespace(types.DefaultKubeVelaNS), client.HasLabels{oam.LabelAddonName, oam.LabelAddonVersion})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, app := range appList.Items {
|
|
addonName := app.Labels[oam.LabelAddonName]
|
|
addonVersion := app.Labels[oam.LabelAddonVersion]
|
|
if addonName == "" || addonVersion == "" {
|
|
continue
|
|
}
|
|
installedAddons[addonName] = ItemInfo{
|
|
Name: addonName,
|
|
AvailableVersions: []string{addonVersion},
|
|
}
|
|
}
|
|
return installedAddons, nil
|
|
}
|
|
|
|
// checkDependencyNeedInstall checks whether dependency addon needs to be reinstalled
|
|
// If the dep addon is not installed, need to install
|
|
// If the dep addon is installed locally, don't need to install
|
|
// If the dep addon is installed from registry and not defined clusters, don't need to install
|
|
// If the dep addon is installed from registry and is defined clusters, and clusters value is nil, don't need to install
|
|
// If the dep addon is installed from registry and is defined clusters, and clusters value is not nil,
|
|
// and the upstream addon's clusters is nil, need to install
|
|
// If the dep addon is installed from registry and is defined clusters, and clusters value is not nil,
|
|
// and the upstream addon's clusters is not nil, the re-installation is based on whether the dep clusters value can contain the upstream clusters value
|
|
func checkDependencyNeedInstall(ctx context.Context, k8sClient client.Client, depName string, addonClusters []string) (bool, error) {
|
|
depApp, err := FetchAddonRelatedApp(ctx, k8sClient, depName)
|
|
if err != nil {
|
|
if !apierrors.IsNotFound(err) {
|
|
return false, err
|
|
}
|
|
// dependent addon is not exist, need to install it
|
|
return true, nil
|
|
}
|
|
|
|
// We should not automatically override addons installed locally by the user, so skip to reinstall it
|
|
labels := depApp.GetLabels()
|
|
installedRegistry := labels[oam.LabelAddonRegistry]
|
|
isLocalRegistry := installedRegistry == LocalAddonRegistryName
|
|
if isLocalRegistry {
|
|
klog.Warningf("%v is installed locally. Please ensure that it has been installed to the required clusters, if not, please manually install it.", depName)
|
|
return false, nil
|
|
}
|
|
|
|
// Addons without the clusters parameter can only be installed on the local cluster. So we don't need to reinstall it
|
|
hasClustersArg, hasClustersArgsErr := hasClustersParameters(ctx, k8sClient, depName)
|
|
if hasClustersArgsErr != nil {
|
|
return false, hasClustersArgsErr
|
|
}
|
|
if !hasClustersArg {
|
|
return false, nil
|
|
}
|
|
|
|
// get addon current parameter value
|
|
depArgs, depArgsErr := GetAddonLegacyParameters(ctx, k8sClient, depName)
|
|
if depArgsErr != nil && !apierrors.IsNotFound(depArgsErr) {
|
|
return false, depArgsErr
|
|
}
|
|
clusterArgValue := depArgs[types.ClustersArg]
|
|
|
|
// nil clusters indicates that the dependent addon is installed on all clusters
|
|
if clusterArgValue == nil {
|
|
return false, nil
|
|
}
|
|
|
|
// nil addonClusters indicates the addon will be installed all clusters, thus the dependent addon should also to be installed on all clusters.
|
|
if addonClusters == nil {
|
|
return true, nil
|
|
}
|
|
|
|
// Determine whether the dependent addon's existing clusters can cover the new addon's clusters
|
|
needInstallAddonDep, _ := hasNotCoveredClusters(clusterArgValue, addonClusters)
|
|
return needInstallAddonDep, nil
|
|
}
|
|
|
|
// getDependencyArgs get the dependent addon's install args according to the upstream addon's clusters parameter's value
|
|
// If dep addon has not defined clusters parameter, don't need to set clusters parameter value,
|
|
// If dep addon has not installed, set the clusters value same to addon's clusters
|
|
// If dep addon clusters parameter's value is nil, the dependent addon is installed on all clusters,
|
|
// don't need to reset clusters parameter value
|
|
// If dep addon has defined clusters parameter, and clusters is not nil, and addon clusters is nil,
|
|
// set clusters value as nil
|
|
// If dep addon has defined clusters parameter, and clusters is not nil, and addon clusters is not nil,
|
|
// set clusters value as the union of the dependent addon's and the upstream addon's clusters
|
|
func getDependencyArgs(ctx context.Context, k8sClient client.Client, depName string, addonClusters []string) (map[string]interface{}, error) {
|
|
hasClustersArg, err := hasClustersParameters(ctx, k8sClient, depName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// dep addon is not install, installed it by assigning clusters value
|
|
_, depErr := FetchAddonRelatedApp(ctx, k8sClient, depName)
|
|
if depErr != nil {
|
|
if !apierrors.IsNotFound(depErr) {
|
|
return nil, depErr
|
|
}
|
|
depArgs := map[string]interface{}{}
|
|
if addonClusters != nil {
|
|
depArgs = map[string]interface{}{
|
|
types.ClustersArg: addonClusters,
|
|
}
|
|
}
|
|
return depArgs, nil
|
|
}
|
|
|
|
// dep addon is installed
|
|
depArgs, depArgsErr := GetAddonLegacyParameters(ctx, k8sClient, depName)
|
|
if depArgsErr != nil && !apierrors.IsNotFound(depArgsErr) {
|
|
return nil, depArgsErr
|
|
}
|
|
if !hasClustersArg || depArgs[types.ClustersArg] == nil {
|
|
return depArgs, nil
|
|
}
|
|
if addonClusters == nil {
|
|
delete(depArgs, types.ClustersArg)
|
|
} else {
|
|
clusterArgValue := depArgs[types.ClustersArg]
|
|
notCovered, depClusters := hasNotCoveredClusters(clusterArgValue, addonClusters)
|
|
if notCovered {
|
|
depArgs[types.ClustersArg] = depClusters
|
|
}
|
|
}
|
|
return depArgs, nil
|
|
}
|
|
|
|
// hasClustersParameters checks whether the addon defines the clusters parameter.
|
|
// If the addon has been installed, get addon package from the installed registry,
|
|
// Otherwise, get addon package from one of registries where this addon exists.
|
|
func hasClustersParameters(ctx context.Context, k8sClient client.Client, addonName string) (bool, error) {
|
|
var installedRegistry []string
|
|
depApp, err := FetchAddonRelatedApp(ctx, k8sClient, addonName)
|
|
if err == nil {
|
|
labels := depApp.GetLabels()
|
|
registryName, ok := labels[oam.LabelAddonRegistry]
|
|
if ok {
|
|
installedRegistry = []string{registryName}
|
|
}
|
|
}
|
|
addonPackages, err := FindAddonPackagesDetailFromRegistry(context.Background(), k8sClient, []string{addonName}, installedRegistry)
|
|
// If the state of addon is not disabled, we don't check the error, because it could be installed from local.
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
var addonPackage *WholeAddonPackage
|
|
if len(addonPackages) != 0 {
|
|
addonPackage = addonPackages[0]
|
|
}
|
|
if addonPackage.APISchema == nil {
|
|
return false, nil
|
|
}
|
|
schemas := addonPackage.APISchema.Properties
|
|
_, hasClusters := schemas[types.ClustersArg]
|
|
return hasClusters, nil
|
|
}
|
|
|
|
// hasNotCoveredClusters check if the clusterArgsValue can cover the values of addonClusters,
|
|
// and if not covered, also return the merged clusters array
|
|
func hasNotCoveredClusters(clusterArgValue interface{}, addonClusters []string) (bool, []string) {
|
|
var needInstallAddonDep = false
|
|
var depClusters []string
|
|
originClusters := clusterArgValue.([]interface{})
|
|
for _, r := range originClusters {
|
|
depClusters = append(depClusters, r.(string))
|
|
}
|
|
for _, addonCluster := range addonClusters {
|
|
if !stringslices.Contains(depClusters, addonCluster) {
|
|
depClusters = append(depClusters, addonCluster)
|
|
needInstallAddonDep = true
|
|
}
|
|
}
|
|
return needInstallAddonDep, depClusters
|
|
}
|
|
|
|
// checkDependency checks if addon's dependency
|
|
func (h *Installer) checkDependency(addon *InstallPackage) ([]string, error) {
|
|
var app v1beta1.Application
|
|
var needEnable []string
|
|
for _, dep := range addon.Dependencies {
|
|
err := h.cli.Get(h.ctx, client.ObjectKey{
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
Name: addonutil.Addon2AppName(dep.Name),
|
|
}, &app)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if !apierrors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
needEnable = append(needEnable, dep.Name)
|
|
}
|
|
return needEnable, nil
|
|
}
|
|
|
|
// createOrUpdate will return true if updated
|
|
func (h *Installer) createOrUpdate(app *v1beta1.Application) (bool, error) {
|
|
// Set the publish version for the addon application
|
|
oam.SetPublishVersion(app, util.GenerateVersion("addon"))
|
|
var existApp v1beta1.Application
|
|
err := h.cli.Get(h.ctx, client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, &existApp)
|
|
if apierrors.IsNotFound(err) {
|
|
return false, h.cli.Create(h.ctx, app)
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
existApp.Spec = app.Spec
|
|
existApp.Labels = app.Labels
|
|
existApp.Annotations = app.Annotations
|
|
err = h.cli.Update(h.ctx, &existApp)
|
|
if err != nil {
|
|
klog.Errorf("fail to create application: %v", err)
|
|
return false, errors.Wrap(err, "fail to create application")
|
|
}
|
|
existApp.DeepCopyInto(app)
|
|
return true, nil
|
|
}
|
|
|
|
func (h *Installer) dispatchAddonResource(ctx context.Context, addon *InstallPackage) error {
|
|
app, auxiliaryOutputs, err := RenderApp(h.ctx, addon, h.cli, h.args)
|
|
if err != nil {
|
|
return errors.Wrap(err, "render addon application fail")
|
|
}
|
|
appName, err := determineAddonAppName(h.ctx, h.cli, h.addon.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
app.Name = appName
|
|
|
|
app.SetLabels(util.MergeMapOverrideWithDst(app.GetLabels(), map[string]string{oam.LabelAddonRegistry: h.r.Name}))
|
|
|
|
// Step1: Render the definitions
|
|
defs, err := RenderDefinitions(addon, h.config)
|
|
if err != nil {
|
|
return errors.Wrap(err, "render addon definitions fail")
|
|
}
|
|
|
|
if !h.overrideDefs {
|
|
existDefs, err := checkConflictDefs(h.ctx, h.cli, defs, app.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(existDefs) != 0 {
|
|
return produceDefConflictError(existDefs)
|
|
}
|
|
}
|
|
|
|
// Step2: Render the config templates
|
|
templates, err := RenderConfigTemplates(ctx, addon, h.cli)
|
|
if err != nil {
|
|
return errors.Wrap(err, "render the config template fail")
|
|
}
|
|
|
|
// Step3: Render the definition schemas
|
|
schemas, err := RenderDefinitionSchema(addon)
|
|
if err != nil {
|
|
return errors.Wrap(err, "render addon definitions' schema fail")
|
|
}
|
|
|
|
// Step4: Render the velaQL views
|
|
views, err := RenderViews(ctx, addon)
|
|
if err != nil {
|
|
return errors.Wrap(err, "render addon views fail")
|
|
}
|
|
|
|
if err := passDefInAppAnnotation(defs, app); err != nil {
|
|
return errors.Wrapf(err, "cannot pass definition to addon app's annotation")
|
|
}
|
|
|
|
if h.dryRun {
|
|
result, err := yaml.Marshal(app)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "dry-run marshal app into yaml %s", app.Name)
|
|
}
|
|
h.dryRunBuff.Write(result)
|
|
h.dryRunBuff.WriteString("\n")
|
|
} else {
|
|
updated, err := h.createOrUpdate(app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if updated {
|
|
h.installerRuntime["upgrade"] = true
|
|
}
|
|
}
|
|
|
|
auxiliaryOutputs = append(auxiliaryOutputs, defs...)
|
|
auxiliaryOutputs = append(auxiliaryOutputs, templates...)
|
|
auxiliaryOutputs = append(auxiliaryOutputs, schemas...)
|
|
auxiliaryOutputs = append(auxiliaryOutputs, views...)
|
|
|
|
for _, o := range auxiliaryOutputs {
|
|
// bind-component means the content is related with the component
|
|
// if component not exists, the resources shouldn't be applied
|
|
if !checkBondComponentExist(*o, *app) {
|
|
continue
|
|
}
|
|
if h.dryRun {
|
|
result, err := yaml.Marshal(o)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "dry-run marshal auxiliary object into yaml %s", o.GetName())
|
|
}
|
|
h.dryRunBuff.WriteString("---\n")
|
|
h.dryRunBuff.Write(result)
|
|
h.dryRunBuff.WriteString("\n")
|
|
continue
|
|
}
|
|
addOwner(o, app)
|
|
err = h.apply.Apply(h.ctx, o, apply.DisableUpdateAnnotation())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if h.dryRun {
|
|
fmt.Print(h.dryRunBuff.String())
|
|
return nil
|
|
}
|
|
|
|
if len(h.args) > 0 {
|
|
sec := RenderArgsSecret(addon, h.args)
|
|
addOwner(sec, app)
|
|
err = h.apply.Apply(h.ctx, sec, apply.DisableUpdateAnnotation())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// delete addon args secret file
|
|
deleteErr := deleteArgsSecret(h.ctx, h.cli, addon.Name)
|
|
if deleteErr != nil {
|
|
return deleteErr
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *Installer) renderNotes(addon *InstallPackage) (string, error) {
|
|
if len(addon.Notes.Data) == 0 {
|
|
return "", nil
|
|
}
|
|
r := addonCueTemplateRender{
|
|
addon: addon,
|
|
inputArgs: h.args,
|
|
contextInfo: map[string]interface{}{
|
|
"installer": h.installerRuntime,
|
|
},
|
|
}
|
|
contextFile, err := r.formatContext()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
notesFile := contextFile + "\n" + addon.Notes.Data
|
|
val := cuecontext.New().CompileString(notesFile)
|
|
if valErr := val.Err(); valErr != nil {
|
|
return "", errors.Wrap(valErr, "build values for NOTES.cue")
|
|
}
|
|
notes := val.LookupPath(cue.ParsePath(KeyWordNotes))
|
|
if !notes.Exists() {
|
|
return "", errors.New("notes not found")
|
|
}
|
|
notesStr, err := notes.String()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "convert notes to string")
|
|
}
|
|
return notesStr, nil
|
|
}
|
|
|
|
// this func will handle such two case
|
|
// 1. if last apply failed an workflow have suspend, this func will continue the workflow
|
|
// 2. restart the workflow, if the new cluster have been added in KubeVela
|
|
func (h *Installer) continueOrRestartWorkflow() error {
|
|
if h.dryRun {
|
|
return nil
|
|
}
|
|
app, err := FetchAddonRelatedApp(h.ctx, h.cli, h.addon.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
// this case means user add a new cluster and user want to restart workflow to dispatch addon resources to new cluster
|
|
// re-apply app won't help app restart workflow
|
|
case app.Status.Phase == common2.ApplicationRunning:
|
|
// we can use retry on conflict here in CLI, because we want to update the status in this CLI operation.
|
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
|
if err = h.cli.Get(h.ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app); err != nil {
|
|
return
|
|
}
|
|
app.Status.Workflow = nil
|
|
return h.cli.Status().Update(h.ctx, app)
|
|
})
|
|
// this case means addon last installation meet some error and workflow has been suspended by app controller
|
|
// re-apply app won't help app workflow continue
|
|
case app.Status.Workflow != nil && app.Status.Workflow.Suspend:
|
|
// we can use retry on conflict here in CLI, because we want to update the status in this CLI operation.
|
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
|
if err = h.cli.Get(h.ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, app); err != nil {
|
|
return
|
|
}
|
|
mergePatch := client.MergeFrom(app.DeepCopy())
|
|
app.Status.Workflow.Suspend = false
|
|
return h.cli.Status().Patch(h.ctx, app, mergePatch)
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getAddonVersionMeetSystemRequirement return the addon's latest version which meet the system requirements
|
|
func (h *Installer) getAddonVersionMeetSystemRequirement(addonName string) string {
|
|
if h.r != nil && IsVersionRegistry(*h.r) {
|
|
versionedRegistry := BuildVersionedRegistry(h.r.Name, h.r.Helm.URL, &common.HTTPOption{
|
|
Username: h.r.Helm.Username,
|
|
Password: h.r.Helm.Password,
|
|
})
|
|
versions, err := versionedRegistry.GetAddonAvailableVersion(addonName)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, version := range versions {
|
|
req := LoadSystemRequirements(version.Annotations)
|
|
if checkAddonVersionMeetRequired(h.ctx, req, h.cli, h.dc) == nil {
|
|
return version.Version
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func addOwner(child *unstructured.Unstructured, app *v1beta1.Application) {
|
|
child.SetOwnerReferences(append(child.GetOwnerReferences(),
|
|
*metav1.NewControllerRef(app, v1beta1.ApplicationKindVersionKind)))
|
|
}
|
|
|
|
// determine app name, if app is already exist, use the application name
|
|
func determineAddonAppName(ctx context.Context, cli client.Client, addonName string) (string, error) {
|
|
app, err := FetchAddonRelatedApp(ctx, cli, addonName)
|
|
if err != nil {
|
|
if !apierrors.IsNotFound(err) {
|
|
return "", err
|
|
}
|
|
// if the app still not exist, use addon-{addonName}
|
|
return addonutil.Addon2AppName(addonName), nil
|
|
}
|
|
return app.Name, nil
|
|
}
|
|
|
|
// FetchAddonRelatedApp will fetch the addon related app, this func will use NamespacedName(vela-system, addon-addonName) to get app
|
|
// if not find will try to get 1.1 legacy addon related app by using NamespacedName(vela-system, `addonName`)
|
|
func FetchAddonRelatedApp(ctx context.Context, cli client.Client, addonName string) (*v1beta1.Application, error) {
|
|
app := &v1beta1.Application{}
|
|
if err := cli.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2AppName(addonName)}, app); err != nil {
|
|
if !apierrors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
// for 1.1 addon app compatibility code
|
|
if err := cli.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: addonName}, app); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return app, nil
|
|
}
|
|
|
|
// checkAddonVersionMeetRequired will check the version of cli/ux and kubevela-core-controller whether meet the addon requirement, if not will return an error
|
|
// please notice that this func is for check production environment which vela cli/ux or vela core is officalVersion
|
|
// if version is for test or debug eg: latest/commit-id/branch-name this func will return nil error
|
|
func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequirements, k8sClient client.Client, dc *discovery.DiscoveryClient) error {
|
|
if require == nil {
|
|
return nil
|
|
}
|
|
|
|
// if not semver version, bypass check cli/ux. eg: {branch name/git commit id/UNKNOWN}
|
|
if version2.IsOfficialKubeVelaVersion(version2.VelaVersion) {
|
|
res, err := checkSemVer(version2.VelaVersion, require.VelaVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !res {
|
|
return fmt.Errorf("vela cli/ux version: %s require: %s", version2.VelaVersion, require.VelaVersion)
|
|
}
|
|
}
|
|
|
|
// check vela core controller version
|
|
imageVersion, err := fetchVelaCoreImageTag(ctx, k8sClient)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if not semver version, bypass check vela-core.
|
|
if version2.IsOfficialKubeVelaVersion(imageVersion) {
|
|
res, err := checkSemVer(imageVersion, require.VelaVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !res {
|
|
return fmt.Errorf("the vela core controller: %s require: %s", imageVersion, require.VelaVersion)
|
|
}
|
|
}
|
|
|
|
// discovery client is nil so bypass check kubernetes version
|
|
if dc == nil {
|
|
return nil
|
|
}
|
|
|
|
k8sVersion, err := dc.ServerVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// if not semver version, bypass check kubernetes version.
|
|
if version2.IsOfficialKubeVelaVersion(k8sVersion.GitVersion) {
|
|
res, err := checkSemVer(k8sVersion.GitVersion, require.KubernetesVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !res {
|
|
return fmt.Errorf("the kubernetes version %s require: %s", k8sVersion.GitVersion, require.KubernetesVersion)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkSemVer(actual string, require string) (bool, error) {
|
|
if len(require) == 0 {
|
|
return true, nil
|
|
}
|
|
semVer := strings.TrimPrefix(actual, "v")
|
|
l := strings.ReplaceAll(require, "v", " ")
|
|
constraint, err := semver.NewConstraint(l)
|
|
if err != nil {
|
|
klog.Errorf("fail to new constraint: %s", err.Error())
|
|
return false, err
|
|
}
|
|
v, err := semver.NewVersion(semVer)
|
|
if err != nil {
|
|
klog.Errorf("fail to new version %s: %s", semVer, err.Error())
|
|
return false, err
|
|
}
|
|
if constraint.Check(v) {
|
|
return true, nil
|
|
}
|
|
if strings.Contains(actual, "-") && !strings.Contains(require, "-") {
|
|
semVer := strings.TrimPrefix(actual[:strings.Index(actual, "-")], "v") // nolint
|
|
if strings.Contains(require, ">=") && require[strings.Index(require, "=")+1:] == semVer {
|
|
// for case: `actual` is 1.5.0-beta.1 require is >=`1.5.0`
|
|
return false, nil
|
|
}
|
|
v, err := semver.NewVersion(semVer)
|
|
if err != nil {
|
|
klog.Errorf("fail to new version %s: %s", semVer, err.Error())
|
|
return false, err
|
|
}
|
|
if constraint.Check(v) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func fetchVelaCoreImageTag(ctx context.Context, k8sClient client.Client) (string, error) {
|
|
deployList := &appsv1.DeploymentList{}
|
|
if err := k8sClient.List(ctx, deployList, client.MatchingLabels{oam.LabelControllerName: oam.ApplicationControllerName}); err != nil {
|
|
return "", err
|
|
}
|
|
deploy := appsv1.Deployment{}
|
|
if len(deployList.Items) == 0 {
|
|
// backward compatible logic old version which vela-core controller has no this label
|
|
if err := k8sClient.Get(ctx, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: types.KubeVelaControllerDeployment}, &deploy); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return "", errors.New("can't find a running KubeVela instance, please install it first")
|
|
}
|
|
return "", err
|
|
}
|
|
} else {
|
|
deploy = deployList.Items[0]
|
|
}
|
|
|
|
var tag string
|
|
for _, c := range deploy.Spec.Template.Spec.Containers {
|
|
if c.Name == types.DefaultKubeVelaReleaseName {
|
|
l := strings.Split(c.Image, ":")
|
|
if len(l) == 1 {
|
|
// if tag is empty mean use latest image
|
|
return "latest", nil
|
|
}
|
|
tag = l[1]
|
|
}
|
|
}
|
|
return tag, nil
|
|
}
|
|
|
|
// PackageAddon package vela addon directory into a helm chart compatible archive and return its absolute path
|
|
func PackageAddon(addonDictPath string) (string, error) {
|
|
// save the Chart.yaml file in order to be compatible with helm chart
|
|
err := MakeChartCompatible(addonDictPath, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ch, err := loader.LoadDir(addonDictPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
dest, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
archive, err := chartutil.Save(ch, dest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return archive, nil
|
|
}
|
|
|
|
// GetAddonLegacyParameters get addon's legacy parameters, that is stored in Secret
|
|
func GetAddonLegacyParameters(ctx context.Context, k8sClient client.Client, addonName string) (map[string]interface{}, error) {
|
|
var sec v1.Secret
|
|
err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: addonutil.Addon2SecName(addonName)}, &sec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args, err := FetchArgsFromSecret(&sec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
// MergeAddonInstallArgs merge addon's legacy parameter and new input args
|
|
func MergeAddonInstallArgs(ctx context.Context, k8sClient client.Client, addonName string, args map[string]interface{}) (map[string]interface{}, error) {
|
|
legacyParams, err := GetAddonLegacyParameters(ctx, k8sClient, addonName)
|
|
if err != nil {
|
|
if !apierrors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
if args == nil && legacyParams == nil {
|
|
return args, nil
|
|
}
|
|
|
|
r := make(map[string]interface{})
|
|
if err := mergo.Merge(&r, legacyParams, mergo.WithOverride); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := mergo.Merge(&r, args, mergo.WithOverride); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|