github.com/oam-dev/kubevela@v1.9.11/pkg/utils/common/common.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package common
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"net/http"
    29  	neturl "net/url"
    30  	"os"
    31  	"os/exec"
    32  	"path/filepath"
    33  	"runtime/debug"
    34  
    35  	"cuelang.org/go/cue"
    36  	"cuelang.org/go/cue/cuecontext"
    37  	"cuelang.org/go/encoding/openapi"
    38  	"github.com/AlecAivazis/survey/v2"
    39  	"github.com/hashicorp/hcl/v2/hclparse"
    40  	"github.com/oam-dev/terraform-config-inspect/tfconfig"
    41  	kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
    42  	kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
    43  	yamlv3 "gopkg.in/yaml.v3"
    44  	v1 "k8s.io/api/core/v1"
    45  	crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    46  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    47  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    48  	"k8s.io/apimachinery/pkg/runtime/schema"
    49  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    50  	apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
    51  	metricsV1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
    52  	ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
    53  	ocmclusterv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1"
    54  	ocmworkv1 "open-cluster-management.io/api/work/v1"
    55  	"sigs.k8s.io/controller-runtime/pkg/client"
    56  	"sigs.k8s.io/controller-runtime/pkg/client/config"
    57  	gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
    58  	"sigs.k8s.io/yaml"
    59  
    60  	"github.com/kubevela/workflow/pkg/cue/model/value"
    61  	"github.com/kubevela/workflow/pkg/cue/packages"
    62  	clustergatewayapi "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
    63  	terraformapiv1 "github.com/oam-dev/terraform-controller/api/v1beta1"
    64  	terraformapi "github.com/oam-dev/terraform-controller/api/v1beta2"
    65  
    66  	workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
    67  
    68  	oamcore "github.com/oam-dev/kubevela/apis/core.oam.dev"
    69  	"github.com/oam-dev/kubevela/apis/types"
    70  	velacue "github.com/oam-dev/kubevela/pkg/cue"
    71  	"github.com/oam-dev/kubevela/pkg/cue/process"
    72  	"github.com/oam-dev/kubevela/pkg/oam"
    73  )
    74  
    75  var (
    76  	// Scheme defines the default KubeVela schema
    77  	Scheme = k8sruntime.NewScheme()
    78  )
    79  
    80  // CreateCustomNamespace display the create namespace message
    81  const CreateCustomNamespace = "create new namespace"
    82  
    83  func init() {
    84  	_ = clientgoscheme.AddToScheme(Scheme)
    85  	_ = apiregistrationv1.AddToScheme(Scheme)
    86  	_ = crdv1.AddToScheme(Scheme)
    87  	_ = oamcore.AddToScheme(Scheme)
    88  	_ = kruise.AddToScheme(Scheme)
    89  	_ = terraformapi.AddToScheme(Scheme)
    90  	_ = terraformapiv1.AddToScheme(Scheme)
    91  	_ = ocmclusterv1alpha1.Install(Scheme)
    92  	_ = ocmclusterv1.Install(Scheme)
    93  	_ = ocmworkv1.Install(Scheme)
    94  	_ = clustergatewayapi.AddToScheme(Scheme)
    95  	_ = metricsV1beta1api.AddToScheme(Scheme)
    96  	_ = kruisev1alpha1.AddToScheme(Scheme)
    97  	_ = gatewayv1beta1.AddToScheme(Scheme)
    98  	_ = workflowv1alpha1.AddToScheme(Scheme)
    99  	// +kubebuilder:scaffold:scheme
   100  }
   101  
   102  // HTTPOption define the https options
   103  type HTTPOption struct {
   104  	Username        string `json:"username,omitempty"`
   105  	Password        string `json:"password,omitempty"`
   106  	CaFile          string `json:"caFile,omitempty"`
   107  	CertFile        string `json:"certFile,omitempty"`
   108  	KeyFile         string `json:"keyFile,omitempty"`
   109  	InsecureSkipTLS bool   `json:"insecureSkipTLS,omitempty"`
   110  }
   111  
   112  // InitBaseRestConfig will return reset config for create controller runtime client
   113  func InitBaseRestConfig() (Args, error) {
   114  	args := Args{
   115  		Schema: Scheme,
   116  	}
   117  	_, err := args.GetConfig()
   118  	if err != nil && os.Getenv("IGNORE_KUBE_CONFIG") != "true" {
   119  		fmt.Println("get kubeConfig err", err)
   120  		os.Exit(1)
   121  	} else if err != nil {
   122  		return Args{}, err
   123  	}
   124  	return args, nil
   125  }
   126  
   127  // HTTPGetResponse use HTTP option and default client to send request and get raw response
   128  func HTTPGetResponse(ctx context.Context, url string, opts *HTTPOption) (*http.Response, error) {
   129  	// Change NewRequest to NewRequestWithContext and pass context it
   130  	if _, err := neturl.ParseRequestURI(url); err != nil {
   131  		return nil, err
   132  	}
   133  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	httpClient := &http.Client{}
   138  	if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 {
   139  		req.SetBasicAuth(opts.Username, opts.Password)
   140  	}
   141  	if opts != nil && opts.InsecureSkipTLS {
   142  		httpClient.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} // nolint
   143  	}
   144  	// if specify the caFile, we cannot re-use the default httpClient, so create a new one.
   145  	if opts != nil && (len(opts.CaFile) != 0 || len(opts.KeyFile) != 0 || len(opts.CertFile) != 0) {
   146  		// must set MinVersion of TLS, otherwise will report GoSec error G402
   147  		tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
   148  		tr := http.Transport{}
   149  		if len(opts.CaFile) != 0 {
   150  			c := x509.NewCertPool()
   151  			if !(c.AppendCertsFromPEM([]byte(opts.CaFile))) {
   152  				return nil, fmt.Errorf("failed to append certificates")
   153  			}
   154  			tlsConfig.RootCAs = c
   155  		}
   156  		if len(opts.CertFile) != 0 && len(opts.KeyFile) != 0 {
   157  			cert, err := tls.X509KeyPair([]byte(opts.CertFile), []byte(opts.KeyFile))
   158  			if err != nil {
   159  				return nil, err
   160  			}
   161  			tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
   162  		}
   163  		tr.TLSClientConfig = tlsConfig
   164  		defer tr.CloseIdleConnections()
   165  		httpClient.Transport = &tr
   166  	}
   167  	return httpClient.Do(req)
   168  }
   169  
   170  // HTTPGetWithOption use HTTP option and default client to send get request
   171  func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) {
   172  	resp, err := HTTPGetResponse(ctx, url, opts)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	//nolint:errcheck
   177  	defer resp.Body.Close()
   178  	return io.ReadAll(resp.Body)
   179  }
   180  
   181  // HTTPGetKubernetesObjects use HTTP requests to load resources from remote url
   182  func HTTPGetKubernetesObjects(ctx context.Context, url string) ([]*unstructured.Unstructured, error) {
   183  	resp, err := HTTPGetResponse(ctx, url, nil)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	//nolint:errcheck
   188  	defer resp.Body.Close()
   189  	decoder := yamlv3.NewDecoder(resp.Body)
   190  	var uns []*unstructured.Unstructured
   191  	for {
   192  		obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
   193  		if err := decoder.Decode(obj.Object); err != nil {
   194  			if errors.Is(err, io.EOF) {
   195  				break
   196  			}
   197  			return nil, fmt.Errorf("failed to decode object: %w", err)
   198  		}
   199  		uns = append(uns, obj)
   200  	}
   201  	return uns, nil
   202  }
   203  
   204  // GetCUEParameterValue converts definitions to cue format
   205  func GetCUEParameterValue(cueStr string, pd *packages.PackageDiscover) (cue.Value, error) {
   206  	template, err := value.NewValue(cueStr+velacue.BaseTemplate, pd, "")
   207  	if err != nil {
   208  		return cue.Value{}, err
   209  	}
   210  	val, err := template.LookupValue(process.ParameterFieldName)
   211  	if err != nil || !val.CueValue().Exists() {
   212  		return cue.Value{}, velacue.ErrParameterNotExist
   213  	}
   214  
   215  	return val.CueValue(), nil
   216  }
   217  
   218  // GenOpenAPI generates OpenAPI json schema from cue.Instance
   219  func GenOpenAPI(val *value.Value) (b []byte, err error) {
   220  	defer func() {
   221  		if r := recover(); r != nil {
   222  			err = fmt.Errorf("invalid cue definition to generate open api: %v", r)
   223  			debug.PrintStack()
   224  			return
   225  		}
   226  	}()
   227  	if val.CueValue().Err() != nil {
   228  		return nil, val.CueValue().Err()
   229  	}
   230  	paramOnlyVal, err := RefineParameterValue(val)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	defaultConfig := &openapi.Config{ExpandReferences: true}
   235  	b, err = openapi.Gen(paramOnlyVal, defaultConfig)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	var out = &bytes.Buffer{}
   240  	_ = json.Indent(out, b, "", "   ")
   241  	return out.Bytes(), nil
   242  }
   243  
   244  // GenOpenAPIWithCueX generates OpenAPI json schema from cue.Instance
   245  func GenOpenAPIWithCueX(val cue.Value) (b []byte, err error) {
   246  	defer func() {
   247  		if r := recover(); r != nil {
   248  			err = fmt.Errorf("invalid cue definition to generate open api: %v", r)
   249  			debug.PrintStack()
   250  			return
   251  		}
   252  	}()
   253  	if val.Err() != nil {
   254  		return nil, val.Err()
   255  	}
   256  	paramOnlyVal := FillParameterDefinitionFieldIfNotExist(val)
   257  	defaultConfig := &openapi.Config{ExpandReferences: true}
   258  	b, err = openapi.Gen(paramOnlyVal, defaultConfig)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	var out = &bytes.Buffer{}
   263  	_ = json.Indent(out, b, "", "   ")
   264  	return out.Bytes(), nil
   265  }
   266  
   267  // RefineParameterValue refines cue value to merely include `parameter` identifier
   268  func RefineParameterValue(val *value.Value) (cue.Value, error) {
   269  	defaultValue := cuecontext.New().CompileString("#parameter: {}")
   270  	parameterPath := cue.MakePath(cue.Def(process.ParameterFieldName))
   271  	v, err := val.MakeValue("{}")
   272  	if err != nil {
   273  		return defaultValue, err
   274  	}
   275  	paramVal, err := val.LookupValue(process.ParameterFieldName)
   276  	if err != nil {
   277  		// nolint:nilerr
   278  		return defaultValue, nil
   279  	}
   280  	switch k := paramVal.CueValue().IncompleteKind(); k {
   281  	case cue.BottomKind:
   282  		return defaultValue, nil
   283  	default:
   284  		paramOnlyVal := v.CueValue().FillPath(parameterPath, paramVal.CueValue())
   285  		return paramOnlyVal, nil
   286  	}
   287  }
   288  
   289  // FillParameterDefinitionFieldIfNotExist refines cue value to merely include `parameter` identifier
   290  func FillParameterDefinitionFieldIfNotExist(val cue.Value) cue.Value {
   291  	defaultValue := cuecontext.New().CompileString("#parameter: {}")
   292  	defPath := cue.ParsePath("#" + process.ParameterFieldName)
   293  	if paramVal := val.LookupPath(cue.ParsePath(process.ParameterFieldName)); paramVal.Exists() {
   294  		if paramVal.IncompleteKind() == cue.BottomKind {
   295  			return defaultValue
   296  		}
   297  		paramOnlyVal := val.Context().CompileString("{}").FillPath(defPath, paramVal)
   298  		return paramOnlyVal
   299  	}
   300  	return defaultValue
   301  }
   302  
   303  // RealtimePrintCommandOutput prints command output in real time
   304  // If logFile is "", it will prints the stdout, or it will write to local file
   305  func RealtimePrintCommandOutput(cmd *exec.Cmd, logFile string) error {
   306  	var writer io.Writer
   307  	if logFile == "" {
   308  		writer = io.MultiWriter(os.Stdout)
   309  	} else {
   310  		if _, err := os.Stat(filepath.Dir(logFile)); err != nil {
   311  			return err
   312  		}
   313  		f, err := os.Create(filepath.Clean(logFile))
   314  		if err != nil {
   315  			return err
   316  		}
   317  		writer = io.MultiWriter(f)
   318  	}
   319  	cmd.Stdout = writer
   320  	cmd.Stderr = writer
   321  	if err := cmd.Run(); err != nil {
   322  		return err
   323  	}
   324  	return nil
   325  }
   326  
   327  // AskToChooseOneNamespace ask for choose one namespace as env
   328  func AskToChooseOneNamespace(c client.Client, envMeta *types.EnvMeta) error {
   329  	var nsList v1.NamespaceList
   330  	if err := c.List(context.TODO(), &nsList); err != nil {
   331  		return err
   332  	}
   333  	var ops = []string{CreateCustomNamespace}
   334  	for _, r := range nsList.Items {
   335  		ops = append(ops, r.Name)
   336  	}
   337  	prompt := &survey.Select{
   338  		Message: "Would you like to choose an existing namespaces as your env?",
   339  		Options: ops,
   340  	}
   341  	err := survey.AskOne(prompt, &envMeta.Namespace)
   342  	if err != nil {
   343  		return fmt.Errorf("choosing namespace err %w", err)
   344  	}
   345  	if envMeta.Namespace == CreateCustomNamespace {
   346  		err = survey.AskOne(&survey.Input{
   347  			Message: "Please name the new namespace:",
   348  		}, &envMeta.Namespace)
   349  		if err != nil {
   350  			return err
   351  		}
   352  		return nil
   353  	}
   354  	for _, ns := range nsList.Items {
   355  		if ns.Name == envMeta.Namespace && envMeta.Name == "" {
   356  			envMeta.Name = ns.Labels[oam.LabelNamespaceOfEnvName]
   357  			return nil
   358  		}
   359  	}
   360  	return nil
   361  }
   362  
   363  // ReadYamlToObject will read a yaml K8s object to runtime.Object
   364  func ReadYamlToObject(path string, object k8sruntime.Object) error {
   365  	data, err := os.ReadFile(filepath.Clean(path))
   366  	if err != nil {
   367  		return err
   368  	}
   369  	return yaml.Unmarshal(data, object)
   370  }
   371  
   372  // ParseTerraformVariables get variables from Terraform Configuration
   373  func ParseTerraformVariables(configuration string) (map[string]*tfconfig.Variable, map[string]*tfconfig.Output, error) {
   374  	p := hclparse.NewParser()
   375  	hclFile, diagnostic := p.ParseHCL([]byte(configuration), "")
   376  	if diagnostic != nil {
   377  		return nil, nil, errors.New(diagnostic.Error())
   378  	}
   379  	mod := tfconfig.Module{Variables: map[string]*tfconfig.Variable{}, Outputs: map[string]*tfconfig.Output{}}
   380  	diagnostic = tfconfig.LoadModuleFromFile(hclFile, &mod)
   381  	if diagnostic != nil {
   382  		return nil, nil, errors.New(diagnostic.Error())
   383  	}
   384  	return mod.Variables, mod.Outputs, nil
   385  }
   386  
   387  // GenerateUnstructuredObj generate UnstructuredObj
   388  func GenerateUnstructuredObj(name, ns string, gvk schema.GroupVersionKind) *unstructured.Unstructured {
   389  	u := &unstructured.Unstructured{}
   390  	u.SetGroupVersionKind(gvk)
   391  	u.SetName(name)
   392  	u.SetNamespace(ns)
   393  	return u
   394  }
   395  
   396  // SetSpecObjIntoUnstructuredObj set UnstructuredObj spec field
   397  func SetSpecObjIntoUnstructuredObj(spec interface{}, u *unstructured.Unstructured) error {
   398  	bts, err := json.Marshal(spec)
   399  	if err != nil {
   400  		return err
   401  	}
   402  	data := make(map[string]interface{})
   403  	if err := json.Unmarshal(bts, &data); err != nil {
   404  		return err
   405  	}
   406  	_ = unstructured.SetNestedMap(u.Object, data, "spec")
   407  	return nil
   408  }
   409  
   410  // NewK8sClient init a local k8s client which add oamcore scheme
   411  func NewK8sClient() (client.Client, error) {
   412  	conf, err := config.GetConfig()
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  	scheme := k8sruntime.NewScheme()
   417  	if err := clientgoscheme.AddToScheme(scheme); err != nil {
   418  		return nil, err
   419  	}
   420  	if err := oamcore.AddToScheme(scheme); err != nil {
   421  		return nil, err
   422  	}
   423  
   424  	k8sClient, err := client.New(conf, client.Options{Scheme: scheme})
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  	return k8sClient, nil
   429  }