Files
kubevela/pkg/utils/common/common.go
T
bcf6af9747 Feat: helm chart auth (#7148)
* feat: kv native helm auth implementation

Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: add e2e test cases for helm auth

Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test: remove the env auth gate and improve test cases

Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* lint: drop unused []byte return from non-docker secret dispatchers

unparam flagged dispatchBasicAuthSecret, dispatchTLSSecret, and
dispatchOpaqueSecret because the second return value (raw config
bytes) was always nil. Only dispatchDockerConfigJSONSecret actually
needs that slot for the OCI temp credfile. Drop it from the other
three and update resolveAuthOptions plus the unit tests to match.

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: pr review changes

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* style: gofmt/goimports auth.go doc comments

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* chore: remove e2e-auth-images-load-local target

The dedicated target is gone; e2e-test-local now reads the same
.vscode/k3d-preload.txt image list that the VS Code setup task
uses so both flows pull from one source. Empty or missing file
is a no-op.

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* chore: inline auth-test image list in e2e-test-local

`.vscode/k3d-preload.txt` is gitignored so it would not exist on a
fresh clone. Inline the three auth-test registry images directly
in the Make target so `make e2e-test-local` is self-contained.

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: address PR review comments

- Webhook ValidateCuexTemplate now wraps the context in helm.WithDryRun
  so a ComponentDefinition with concrete helm.#Render arguments cannot
  trigger a real chart fetch or install during admission validation.
- helmchart audit ConfigMap keeps `helm.oam.dev/chart` as a label when
  the source string is a valid Kubernetes label value (alphanumeric +
  `.-_`, 1-63 chars); URLs containing `://` or `/` only live in the
  annotation. Preserves existing label selectors for repo-style sources
  without breaking long OCI/HTTPS URLs.
- helm_test.go capture-and-restore singleton.KubeClient in fetchURLChart
  and fetchRepoChart auth Describes so fake clients do not leak into
  later tests in the package.
- utils_test.go swap the defer order so singleton.ReloadClients runs
  before WorkloadCompiler.Reload, otherwise the compiler reload would
  pick up the fake dynamic client and leak fake state.
- e2e.mk pre-load loop separates docker pull and k3d image import with
  `;` rather than `&&`. `set -e` does not abort a for-loop body when a
  command inside an `&&` chain fails, so the old form could continue
  after a failed pull.

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* style: gofmt AfterAll indentation in helmchart_test.go

Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: enhance Docker Hub credential handling in auth configuration

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* feat: enhance Docker Hub credential handling in auth configuration

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* fix(helm-auth): normalize Docker Hub alias for dockerconfigjson Secrets

Cubic flagged that the Docker Hub alias fix in writeOCIRegistryConfigFile
only covered synthesized basic-auth credentials. For verbatim
kubernetes.io/dockerconfigjson Secrets, a user-supplied config keyed
under "registry-1.docker.io" (the OCI pull host) would not be found by
ORAS/Helm, which normalizes to "https://index.docker.io/v1/".

normalizeDockerHubAliases parses the verbatim JSON, and when any of the
three Docker Hub host aliases ("registry-1.docker.io", "index.docker.io",
"docker.io") is keyed but the canonical v1 key is absent, copies the
entry under the canonical key. No-op when:
  - the canonical key is already present
  - no Docker Hub host is involved
  - the JSON is malformed or missing the auths field

Includes 7 new unit specs for the helper covering each alias, the no-op
cases, and malformed input.

Also brings in collateral local-fix changes:
  - pkg/webhook/utils/utils_test.go: register the cue.oam.dev/v1alpha1
    Package GVK in the fake DynamicClient scheme so
    TestValidateCuexTemplate/withCuexPackageImports can resolve the
    test/ext package after the WorkloadCompiler switch.
  - gofmt field-alignment normalization on auth_test.go and
    auth_registry_helpers_test.go (drift caught by check-diff CI).

Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* fix: namespace-default rendered resources, bind chart cache to credentials, surface HTTP 401s clearly, inject auth-test CA in e2e

Four fixes from stress testing the helmchart component:

1. Rendered resources without metadata.namespace landed in vela-system.
   Both code paths now default to releaseNamespace for namespaced kinds:
   the velaLabelPostRenderer (before helm SDK apply) and
   parseManifestResources (before KubeVela's tracker re-applies).
   isClusterScopedKind covers CRDs, ClusterRoles, Namespaces, etc.

2. Cached chart bytes survived credential rotation.
   computeAuthCacheTag hashes Secret.Type + sorted Data into a 16-char
   tag suffixed onto the cache key when auth.secretRef is declared. Any
   Secret edit (or different Secret reference) invalidates the cache and
   forces a fresh registry call that exercises the new credentials at the
   wire. Public charts (no auth) are unaffected.

3. HTTP 401/403 on chart fetch surfaced as YAML/JSON parse errors.
   HTTPGetWithOption now rejects non-2xx responses with
   'HTTP <status>: <body>' instead of returning the raw body for
   downstream parsers to choke on.

4. E2E HTTPS tests against chartmuseum's self-signed cert failed at the
   admission webhook. injectAuthTestCA patches vela-core in BeforeSuite
   with an init container that combines /etc/ssl/certs/ca-certificates.crt
   with testdata/auth/certs/ca.crt into a shared volume and points
   SSL_CERT_FILE at it.

Also normalize Docker Hub host aliases on verbatim
kubernetes.io/dockerconfigjson Secrets (cubic comment), and revert
TestValidateCuexTemplate's fake DynamicClient to the empty-scheme form
that lets it wrap every GVK as Unstructured (CI unit test fix).

Finding 3 (options.wait default) is deferred: schema-level defaults
don't materialize for optional fields carried into _options as a
structural copy. Tracked separately.

Issue 2 from the stress matrix (cache + credential rotation) is also
filed upstream as kubevela/kubevela#7150 for a longer-term
credential-bound cache invalidation design.

Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* fix(helm-auth): RESTMapper scope detection + auth tag binds source URL + drop flaky webhook test case

Address cubic P1 review comments and the unit-test CI failure. isClusterScopedKind only matched well-known built-in kinds; isClusterScopedGVK now asks the RESTMapper first so third-party cluster-scoped CRDs are recognised, with the static allowlist as a fallback. computeAuthCacheTag now folds params.Source and params.RepoURL into the hash so a multi-host dockerconfigjson Secret cannot reuse cached bytes across different registries. TestValidateCuexTemplate/withCuexPackageImports relied on cuex.DefaultCompiler.Reload picking up a fake-client-served Package CRD; since ValidateCuexTemplate now uses velacuex.WorkloadCompiler the fake-client setup does not surface the test/ext package, so the case is dropped (covered transitively by the helm provider unit tests and the e2e suite under helm.WithDryRun).

Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* style: gofmt import order in helm.go (lint + check-diff)

Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* fix(e2e): correct vela-core selector, alpine bundler, harden rollout wait

The label selector app.kubernetes.io/name=vela-core also matches the
cluster-gateway Deployment, so the init container was being appended to
the wrong workload and the controller pod never picked up the test CA.
Switching to the unique controller.oam.dev/name label fixes the routing,
and the diagnostic message at the empty-list branch is updated to point
operators at the new label.

addVolume and addInitContainer now update in place when an entry with
the same name already exists. Previously they silently skipped, which
meant the first patch on a cluster stuck for the lifetime of that
cluster and a corrected image or args could not be picked up by a
re-run.

busybox:1.36 ships without /etc/ssl/certs/ca-certificates.crt, so the
bundler was silently producing an empty combined.crt. alpine:3.18 ships
the public CA roots, so concatenating the auth-test CA onto the system
bundle produces a usable trust store that the main container reads via
SSL_CERT_FILE.

waitForDeploymentsAvailable used to return as soon as DeploymentAvailable
flipped to True and the new RS reached the desired replica count, which
left a window where old-RS pods still satisfied Available while the
controller container was still running with the previous image. The
check now also requires Status.Replicas == Status.UpdatedReplicas,
AvailableReplicas >= specReplicas and UnavailableReplicas == 0, so the
helper only returns once the rollout is fully complete.

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>

* test(e2e): gate auth-test setup behind KUBEVELA_E2E_AUTH so other suites stay green

webhook-upgrade-check runs the same test/e2e-test suite as the full e2e
job but with ginkgo --focus-file requiredparam_validation_test.go. The
unconditional setupAuthRegistries in BeforeSuite was bringing up the
ChartMuseum / zot / nginx-bearer stack and patching vela-core for a
focus that did not need any of it, and the patched controller never
finished rolling on the upgrade-check cluster (the helm upgrade in that
job is expected to fail, so vela-core is in a stretched state when our
BeforeSuite tries to patch it).

Re-introduce KUBEVELA_E2E_AUTH=1 as the explicit opt-in. BeforeSuite and
AfterSuite only touch the auth registries when the env var is set, the
Helmchart Auth Describe block skips otherwise, and the make e2e-test
and e2e-test-local targets export the env so the full suite still runs
the auth specs. CI workflows that run a focused subset (like
webhook-upgrade-check) inherit the default off behavior and stop paying
the registry-setup cost.

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>

---------

Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Signed-off-by: Vishal Kumar <vishal210893@gmail.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Vishal Kumar <vishal210893@gmail.com>
2026-05-22 09:09:55 -07:00

468 lines
15 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 common
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
neturl "net/url"
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/encoding/openapi"
"github.com/AlecAivazis/survey/v2"
"github.com/hashicorp/hcl/v2/hclparse"
cuexv1alpha1 "github.com/kubevela/pkg/apis/cue/v1alpha1"
"github.com/kubevela/pkg/cue/cuex"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
clustergatewayapi "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
"github.com/oam-dev/terraform-config-inspect/tfconfig"
terraformapiv1 "github.com/oam-dev/terraform-controller/api/v1beta1"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta2"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
yamlv3 "go.yaml.in/yaml/v3"
v1 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
metricsV1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1"
ocmworkv1 "open-cluster-management.io/api/work/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
"sigs.k8s.io/yaml"
oamcore "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/apis/types"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/oam"
)
var (
// Scheme defines the default KubeVela schema
Scheme = k8sruntime.NewScheme()
)
// CreateCustomNamespace display the create namespace message
const CreateCustomNamespace = "create new namespace"
func init() {
_ = clientgoscheme.AddToScheme(Scheme)
_ = apiregistrationv1.AddToScheme(Scheme)
_ = crdv1.AddToScheme(Scheme)
_ = oamcore.AddToScheme(Scheme)
_ = kruise.AddToScheme(Scheme)
_ = terraformapi.AddToScheme(Scheme)
_ = terraformapiv1.AddToScheme(Scheme)
_ = ocmclusterv1alpha1.Install(Scheme)
_ = ocmclusterv1.Install(Scheme)
_ = ocmworkv1.Install(Scheme)
_ = clustergatewayapi.AddToScheme(Scheme)
_ = metricsV1beta1api.AddToScheme(Scheme)
_ = kruisev1alpha1.AddToScheme(Scheme)
_ = gatewayv1beta1.AddToScheme(Scheme)
_ = workflowv1alpha1.AddToScheme(Scheme)
_ = cuexv1alpha1.AddToScheme(Scheme)
// +kubebuilder:scaffold:scheme
}
// HTTPOption define the https options
type HTTPOption struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
BearerToken string `json:"bearerToken,omitempty"` // RFC 6750. Mutually exclusive with Username/Password.
CaFile string `json:"caFile,omitempty"`
CertFile string `json:"certFile,omitempty"`
KeyFile string `json:"keyFile,omitempty"`
InsecureSkipTLS bool `json:"insecureSkipTLS,omitempty"`
// PlainHTTP signals that the caller wants the OCI client to use plain
// HTTP rather than TLS. Honored only on the OCI fetch path. Insecure
// by design; users opt in via the Opaque Secret key insecurePlainHTTP.
PlainHTTP bool `json:"plainHTTP,omitempty"`
}
// InitBaseRestConfig will return reset config for create controller runtime client
func InitBaseRestConfig() (Args, error) {
args := Args{
Schema: Scheme,
}
_, err := args.GetConfig()
if err != nil && os.Getenv("IGNORE_KUBE_CONFIG") != "true" {
fmt.Println("get kubeConfig err", err)
os.Exit(1)
} else if err != nil {
return Args{}, err
}
return args, nil
}
// HTTPGetResponse use HTTP option and default client to send request and get raw response
func HTTPGetResponse(ctx context.Context, url string, opts *HTTPOption) (*http.Response, error) {
// Change NewRequest to NewRequestWithContext and pass context it
if _, err := neturl.ParseRequestURI(url); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
httpClient := &http.Client{}
if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 {
req.SetBasicAuth(opts.Username, opts.Password)
}
if opts != nil && opts.BearerToken != "" {
if opts.Username != "" || opts.Password != "" {
return nil, fmt.Errorf(
"HTTPOption sets both basic-auth and a bearer token: " +
"at most one credential method MUST be configured (RFC 6750 §2)")
}
req.Header.Set("Authorization", "Bearer "+opts.BearerToken)
}
if opts != nil && opts.InsecureSkipTLS {
httpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} // nolint
}
// if specify the caFile, we cannot re-use the default httpClient, so create a new one.
if opts != nil && (len(opts.CaFile) != 0 || len(opts.KeyFile) != 0 || len(opts.CertFile) != 0) {
// must set MinVersion of TLS, otherwise will report GoSec error G402
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
tr := http.Transport{}
if len(opts.CaFile) != 0 {
c := x509.NewCertPool()
if !(c.AppendCertsFromPEM([]byte(opts.CaFile))) {
return nil, fmt.Errorf("failed to append certificates")
}
tlsConfig.RootCAs = c
}
if len(opts.CertFile) != 0 && len(opts.KeyFile) != 0 {
cert, err := tls.X509KeyPair([]byte(opts.CertFile), []byte(opts.KeyFile))
if err != nil {
return nil, err
}
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
}
tr.TLSClientConfig = tlsConfig
defer tr.CloseIdleConnections()
httpClient.Transport = &tr
}
return httpClient.Do(req)
}
// HTTPGetWithOption use HTTP option and default client to send get request.
// Non-2xx responses are surfaced as an error including the status line plus a
// truncated body excerpt. Without this guard, registry 401/403 bodies (HTML
// or short text) would be returned as raw bytes and later parsed as YAML or
// gzip-tar, producing misleading "no chart name found" / "cannot unmarshal
// string into Go value of type repo.IndexFile" failures instead of a clear
// "HTTP 401 Unauthorized" message.
func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) {
resp, err := HTTPGetResponse(ctx, url, opts)
if err != nil {
return nil, err
}
//nolint:errcheck
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
excerpt := strings.TrimSpace(string(body))
if excerpt == "" {
return nil, fmt.Errorf("HTTP %s", resp.Status)
}
return nil, fmt.Errorf("HTTP %s: %s", resp.Status, excerpt)
}
return io.ReadAll(resp.Body)
}
// HTTPGetKubernetesObjects use HTTP requests to load resources from remote url
func HTTPGetKubernetesObjects(ctx context.Context, url string) ([]*unstructured.Unstructured, error) {
resp, err := HTTPGetResponse(ctx, url, nil)
if err != nil {
return nil, err
}
//nolint:errcheck
defer resp.Body.Close()
decoder := yamlv3.NewDecoder(resp.Body)
var uns []*unstructured.Unstructured
for {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := decoder.Decode(obj.Object); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("failed to decode object: %w", err)
}
uns = append(uns, obj)
}
return uns, nil
}
// GetCUEParameterValue converts definitions to cue format
func GetCUEParameterValue(cueStr string) (cue.Value, error) {
template := cuecontext.New().CompileString(cueStr + velacue.BaseTemplate)
val := template.LookupPath(cue.ParsePath(process.ParameterFieldName))
if !val.Exists() {
return cue.Value{}, velacue.ErrParameterNotExist
}
return val, nil
}
// GetCUExParameterValue converts definitions with cuex imports to cue format and extracts parameter field.
// An optional *cuex.Compiler can be passed to override the default compiler (e.g. to use one that has
// additional internal packages such as vela/builtin or vela/multicluster registered).
func GetCUExParameterValue(ctx context.Context, cueStr string, compilers ...*cuex.Compiler) (cue.Value, error) {
compiler := cuex.DefaultCompiler.Get()
if len(compilers) > 0 && compilers[0] != nil {
compiler = compilers[0]
}
template, err := compiler.CompileStringWithOptions(
ctx,
cueStr+velacue.BaseTemplate,
cuex.DisableResolveProviderFunctions{},
)
if err != nil {
return cue.Value{}, err
}
val := template.LookupPath(cue.ParsePath(process.ParameterFieldName))
if !val.Exists() {
return cue.Value{}, velacue.ErrParameterNotExist
}
return val, nil
}
// GenOpenAPI generates OpenAPI json schema from cue.Instance
func GenOpenAPI(val cue.Value) (b []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid cue definition to generate open api: %v", r)
debug.PrintStack()
return
}
}()
if val.Err() != nil {
return nil, val.Err()
}
paramOnlyVal, err := RefineParameterValue(val)
if err != nil {
return nil, err
}
defaultConfig := &openapi.Config{ExpandReferences: true}
b, err = openapi.Gen(paramOnlyVal, defaultConfig)
if err != nil {
return nil, err
}
var out = &bytes.Buffer{}
_ = json.Indent(out, b, "", " ")
return out.Bytes(), nil
}
// GenOpenAPIWithCueX generates OpenAPI json schema from cue.Instance
func GenOpenAPIWithCueX(val cue.Value) (b []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid cue definition to generate open api: %v", r)
debug.PrintStack()
return
}
}()
if val.Err() != nil {
return nil, val.Err()
}
paramOnlyVal := FillParameterDefinitionFieldIfNotExist(val)
defaultConfig := &openapi.Config{ExpandReferences: true}
b, err = openapi.Gen(paramOnlyVal, defaultConfig)
if err != nil {
return nil, err
}
var out = &bytes.Buffer{}
_ = json.Indent(out, b, "", " ")
return out.Bytes(), nil
}
// RefineParameterValue refines cue value to merely include `parameter` identifier
func RefineParameterValue(val cue.Value) (cue.Value, error) {
cuectx := val.Context()
defaultValue := cuectx.CompileString("#parameter: {}")
parameterPath := cue.MakePath(cue.Def(process.ParameterFieldName))
v := cuectx.CompileString("{}")
paramVal := val.LookupPath(cue.ParsePath(process.ParameterFieldName))
switch k := paramVal.IncompleteKind(); k {
case cue.BottomKind:
return defaultValue, nil
default:
paramOnlyVal := v.FillPath(parameterPath, paramVal)
return paramOnlyVal, nil
}
}
// FillParameterDefinitionFieldIfNotExist refines cue value to merely include `parameter` identifier
func FillParameterDefinitionFieldIfNotExist(val cue.Value) cue.Value {
defaultValue := cuecontext.New().CompileString("#parameter: {}")
defPath := cue.ParsePath("#" + process.ParameterFieldName)
if paramVal := val.LookupPath(cue.ParsePath(process.ParameterFieldName)); paramVal.Exists() {
if paramVal.IncompleteKind() == cue.BottomKind {
return defaultValue
}
paramOnlyVal := val.Context().CompileString("{}").FillPath(defPath, paramVal)
return paramOnlyVal
}
return defaultValue
}
// RealtimePrintCommandOutput prints command output in real time
// If logFile is "", it will prints the stdout, or it will write to local file
func RealtimePrintCommandOutput(cmd *exec.Cmd, logFile string) error {
var writer io.Writer
if logFile == "" {
writer = io.MultiWriter(os.Stdout)
} else {
if _, err := os.Stat(filepath.Dir(logFile)); err != nil {
return err
}
f, err := os.Create(filepath.Clean(logFile))
if err != nil {
return err
}
writer = io.MultiWriter(f)
}
cmd.Stdout = writer
cmd.Stderr = writer
return cmd.Run()
}
// AskToChooseOneNamespace ask for choose one namespace as env
func AskToChooseOneNamespace(c client.Client, envMeta *types.EnvMeta) error {
var nsList v1.NamespaceList
if err := c.List(context.TODO(), &nsList); err != nil {
return err
}
var ops = []string{CreateCustomNamespace}
for _, r := range nsList.Items {
ops = append(ops, r.Name)
}
prompt := &survey.Select{
Message: "Would you like to choose an existing namespaces as your env?",
Options: ops,
}
err := survey.AskOne(prompt, &envMeta.Namespace)
if err != nil {
return fmt.Errorf("choosing namespace err %w", err)
}
if envMeta.Namespace == CreateCustomNamespace {
err = survey.AskOne(&survey.Input{
Message: "Please name the new namespace:",
}, &envMeta.Namespace)
if err != nil {
return err
}
return nil
}
for _, ns := range nsList.Items {
if ns.Name == envMeta.Namespace && envMeta.Name == "" {
envMeta.Name = ns.Labels[oam.LabelNamespaceOfEnvName]
return nil
}
}
return nil
}
// ReadYamlToObject will read a yaml K8s object to runtime.Object
func ReadYamlToObject(path string, object k8sruntime.Object) error {
data, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return err
}
return yaml.Unmarshal(data, object)
}
// ParseTerraformVariables get variables from Terraform Configuration
func ParseTerraformVariables(configuration string) (map[string]*tfconfig.Variable, map[string]*tfconfig.Output, error) {
p := hclparse.NewParser()
hclFile, diagnostic := p.ParseHCL([]byte(configuration), "")
if diagnostic != nil {
return nil, nil, errors.New(diagnostic.Error())
}
mod := tfconfig.Module{Variables: map[string]*tfconfig.Variable{}, Outputs: map[string]*tfconfig.Output{}}
diagnostic = tfconfig.LoadModuleFromFile(hclFile, &mod)
if diagnostic != nil {
return nil, nil, errors.New(diagnostic.Error())
}
return mod.Variables, mod.Outputs, nil
}
// GenerateUnstructuredObj generate UnstructuredObj
func GenerateUnstructuredObj(name, ns string, gvk schema.GroupVersionKind) *unstructured.Unstructured {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(gvk)
u.SetName(name)
u.SetNamespace(ns)
return u
}
// SetSpecObjIntoUnstructuredObj set UnstructuredObj spec field
func SetSpecObjIntoUnstructuredObj(spec interface{}, u *unstructured.Unstructured) error {
bts, err := json.Marshal(spec)
if err != nil {
return err
}
data := make(map[string]interface{})
if err := json.Unmarshal(bts, &data); err != nil {
return err
}
_ = unstructured.SetNestedMap(u.Object, data, "spec")
return nil
}
// NewK8sClient init a local k8s client which add oamcore scheme
func NewK8sClient() (client.Client, error) {
conf, err := config.GetConfig()
if err != nil {
return nil, err
}
scheme := k8sruntime.NewScheme()
if err := clientgoscheme.AddToScheme(scheme); err != nil {
return nil, err
}
if err := oamcore.AddToScheme(scheme); err != nil {
return nil, err
}
k8sClient, err := client.New(conf, client.Options{Scheme: scheme})
if err != nil {
return nil, err
}
return k8sClient, nil
}