diff --git a/cmd/build.go b/cmd/build.go index 1c16cdce9b..a37a91e246 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -15,6 +15,7 @@ import ( "knative.dev/func/pkg/config" "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/oci" "knative.dev/func/pkg/s2i" ) @@ -171,8 +172,12 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro f = cfg.Configure(f) // Returns an f updated with values from the config (flags, envs, etc) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + + f.Registry = resolveRegistry(f.Registry, "", kc) + // Client - clientOptions, err := cfg.clientOptions() + clientOptions, err := cfg.clientOptions(kc) if err != nil { return } @@ -214,6 +219,14 @@ func warnRegistryInsecureChange(w io.Writer, newRegistry string, f fn.Function) } } +// warnClusterChange determines if the cluster is being changed deliberately and +// if so, warn the user that creds might need to be added +func warnClusterChange(w io.Writer, newCluster string, oldCluster string) { + if newCluster != "" && oldCluster != "" && newCluster != oldCluster { + fmt.Fprintf(w, "Warning: changing cluster from '%s' to '%s'. Ensure your credentials are valid for the new cluster.\n", oldCluster, newCluster) + } +} + type buildConfig struct { // Globals (builder, confirm, registry, verbose) config.Global @@ -264,8 +277,9 @@ func newBuildConfig() buildConfig { return buildConfig{ Global: config.Global{ Builder: viper.GetString("builder"), + Cluster: viper.GetString("cluster"), Confirm: viper.GetBool("confirm"), - Registry: registry(), // deferred defaulting + Registry: viper.GetString("registry"), Verbose: viper.GetBool("verbose"), RegistryInsecure: viper.GetBool("registry-insecure"), }, @@ -441,14 +455,14 @@ func (c buildConfig) Validate(cmd *cobra.Command) (err error) { // TODO: As a further optimization, it might be ideal to only build the // image necessary for the target cluster, since the end product of a function // deployment is not the container, but rather the running service. -func (c buildConfig) clientOptions() ([]fn.Option, error) { +func (c buildConfig) clientOptions(kc *k8s.Client) ([]fn.Option, error) { o := []fn.Option{ fn.WithRegistry(c.Registry), fn.WithRegistryInsecure(c.RegistryInsecure), } - t := newTransport(c.RegistryInsecure) - creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile, c.RegistryInsecure) + t := newTransport(c.RegistryInsecure, kc) + creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile, c.RegistryInsecure, kc) switch c.Builder { case builders.Host: @@ -456,7 +470,7 @@ func (c buildConfig) clientOptions() ([]fn.Option, error) { fn.WithScaffolder(oci.NewScaffolder(c.Verbose)), fn.WithBuilder(oci.NewBuilder(builders.Host, c.Verbose)), fn.WithPusher(oci.NewPusher(c.RegistryInsecure, false, c.Verbose, - oci.WithTransport(newTransport(c.RegistryInsecure)), + oci.WithTransport(newTransport(c.RegistryInsecure, kc)), oci.WithCredentialsProvider(creds), oci.WithVerbose(c.Verbose))), ) diff --git a/cmd/client.go b/cmd/client.go index c4acf0802f..9fe17f75b4 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -32,6 +32,9 @@ type ClientConfig struct { // Allow insecure server connections when using SSL InsecureSkipVerify bool + + // Constructed k8s client config to be used with optional overrides + K8sClient *k8s.Client } // ClientFactory defines a constructor which assists in the creation of a Client @@ -59,24 +62,28 @@ func NewTestClient(options ...fn.Option) ClientFactory { // the currently configured is used. // 'Verbose' indicates the system should write out a higher amount of logging. func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { + kc := cfg.K8sClient + if kc == nil { + kc = k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + } var ( - t = newTransport(cfg.InsecureSkipVerify) // may provide a custom impl which proxies - c = newCredentialsProvider(config.Dir(), t, "", cfg.InsecureSkipVerify) // for accessing registries - d = newKnativeDeployer(cfg.Verbose) // default deployer (can be overridden via options) - pp = newTektonPipelinesProvider(c, cfg.Verbose, t) + t = newTransport(cfg.InsecureSkipVerify, kc) // may provide a custom impl which proxies + c = newCredentialsProvider(config.Dir(), t, "", cfg.InsecureSkipVerify, kc) // for accessing registries + d = newKnativeDeployer(cfg.Verbose, kc) // default deployer (can be overridden via options) + pp = newTektonPipelinesProvider(c, cfg.Verbose, t, kc) o = []fn.Option{ // standard (shared) options for all commands fn.WithVerbose(cfg.Verbose), fn.WithTransport(t), fn.WithRepositoriesPath(config.RepositoriesPath()), fn.WithScaffolder(buildpacks.NewScaffolder(cfg.Verbose)), fn.WithBuilder(buildpacks.NewBuilder(buildpacks.WithVerbose(cfg.Verbose))), - fn.WithRemovers(knative.NewRemover(cfg.Verbose), k8s.NewRemover(cfg.Verbose), keda.NewRemover(cfg.Verbose)), + fn.WithRemovers(knative.NewRemover(kc, cfg.Verbose), k8s.NewRemover(kc, cfg.Verbose), keda.NewRemover(kc, cfg.Verbose)), fn.WithDescribers( - knative.NewDescriber(cfg.Verbose, knative.WithDescriberTransport(t)), - k8s.NewDescriber(cfg.Verbose, k8s.WithDescriberTransport(t)), - keda.NewDescriber(cfg.Verbose, keda.WithDescriberTransport(t)), + knative.NewDescriber(kc, cfg.Verbose, knative.WithDescriberTransport(t)), + k8s.NewDescriber(kc, cfg.Verbose, k8s.WithDescriberTransport(t)), + keda.NewDescriber(kc, cfg.Verbose, keda.WithDescriberTransport(t)), ), - fn.WithListers(knative.NewLister(cfg.Verbose), k8s.NewLister(cfg.Verbose), keda.NewLister(cfg.Verbose)), + fn.WithListers(knative.NewLister(kc, cfg.Verbose), k8s.NewLister(kc, cfg.Verbose), keda.NewLister(kc, cfg.Verbose)), fn.WithDeployer(d), fn.WithPipelinesProvider(pp), fn.WithPusher(docker.NewPusher( @@ -84,7 +91,7 @@ func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { docker.WithTransport(t), docker.WithVerbose(cfg.Verbose), docker.WithInsecure(cfg.InsecureSkipVerify))), - fn.WithSyncer(operator.NewSyncer(operator.WithCredentialsProvider(c))), + fn.WithSyncer(operator.NewSyncer(kc, operator.WithCredentialsProvider(c))), } ) @@ -105,8 +112,8 @@ func NewClient(cfg ClientConfig, options ...fn.Option) (*fn.Client, func()) { // newTransport returns a transport with cluster-flavor-specific variations // which take advantage of additional features offered by cluster variants. -func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser { - return fnhttp.NewRoundTripper(fnhttp.WithInsecureSkipVerify(insecureSkipVerify), fnhttp.WithOpenShiftServiceCA()) +func newTransport(insecureSkipVerify bool, c *k8s.Client) fnhttp.RoundTripCloser { + return fnhttp.NewRoundTripper(c, fnhttp.WithInsecureSkipVerify(insecureSkipVerify), fnhttp.WithOpenShiftServiceCA(c)) } // newCredentialsProvider returns a credentials provider which possibly @@ -114,8 +121,8 @@ func newTransport(insecureSkipVerify bool) fnhttp.RoundTripCloser { // of features or configuration nuances of cluster variants. // If authFilePath is provided (non-empty), it will be used as the primary auth file. // When insecure is true, credential verification uses plain HTTP instead of HTTPS. -func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string, insecure bool) oci.CredentialsProvider { - additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(), k8s.GetGoogleCredentialLoader()...) +func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath string, insecure bool, k8sClient *k8s.Client) oci.CredentialsProvider { + additionalLoaders := append(k8s.GetOpenShiftDockerCredentialLoaders(k8sClient), k8s.GetGoogleCredentialLoader()...) additionalLoaders = append(additionalLoaders, k8s.GetECRCredentialLoader()...) additionalLoaders = append(additionalLoaders, k8s.GetACRCredentialLoader()...) @@ -152,57 +159,58 @@ func newCredentialsProvider(configPath string, t http.RoundTripper, authFilePath return creds.NewCredentialsProvider(configPath, options...) } -func newTektonPipelinesProvider(creds oci.CredentialsProvider, verbose bool, transport http.RoundTripper) *tekton.PipelinesProvider { +func newTektonPipelinesProvider(creds oci.CredentialsProvider, verbose bool, transport http.RoundTripper, kc *k8s.Client) *tekton.PipelinesProvider { options := []tekton.Opt{ tekton.WithCredentialsProvider(creds), tekton.WithVerbose(verbose), - tekton.WithPipelineDecorator(deployDecorator{}), + tekton.WithPipelineDecorator(deployDecorator{k8sClient: kc}), tekton.WithTransport(transport), } - return tekton.NewPipelinesProvider(options...) + return tekton.NewPipelinesProvider(kc, options...) } -func newKnativeDeployer(verbose bool) fn.Deployer { +func newKnativeDeployer(verbose bool, kc *k8s.Client) fn.Deployer { options := []knative.DeployerOpt{ knative.WithDeployerVerbose(verbose), - knative.WithDeployerDecorator(deployDecorator{}), + knative.WithDeployerDecorator(deployDecorator{k8sClient: kc}), } - return knative.NewDeployer(options...) + return knative.NewDeployer(kc, options...) } -func newK8sDeployer(verbose bool) fn.Deployer { +func newK8sDeployer(verbose bool, kc *k8s.Client) fn.Deployer { options := []k8s.DeployerOpt{ k8s.WithDeployerVerbose(verbose), - k8s.WithDeployerDecorator(deployDecorator{}), + k8s.WithDeployerDecorator(deployDecorator{k8sClient: kc}), } - return k8s.NewDeployer(options...) + return k8s.NewDeployer(kc, options...) } -func newKedaDeployer(verbose bool) fn.Deployer { +func newKedaDeployer(verbose bool, kc *k8s.Client) fn.Deployer { options := []keda.DeployerOpt{ keda.WithDeployerVerbose(verbose), - keda.WithDeployerDecorator(deployDecorator{}), + keda.WithDeployerDecorator(deployDecorator{k8sClient: kc}), } - return keda.NewDeployer(options...) + return keda.NewDeployer(kc, options...) } type deployDecorator struct { - oshDec k8s.OpenshiftMetadataDecorator + k8sClient *k8s.Client + oshDec k8s.OpenshiftMetadataDecorator } func (d deployDecorator) UpdateAnnotations(function fn.Function, annotations map[string]string) map[string]string { - if k8s.IsOpenShift() { + if d.k8sClient != nil && d.k8sClient.IsOpenshift() { return d.oshDec.UpdateAnnotations(function, annotations) } return annotations } func (d deployDecorator) UpdateLabels(function fn.Function, labels map[string]string) map[string]string { - if k8s.IsOpenShift() { + if d.k8sClient != nil && d.k8sClient.IsOpenshift() { return d.oshDec.UpdateLabels(function, labels) } return labels diff --git a/cmd/completion_util.go b/cmd/completion_util.go index cffb092534..f2515a61d8 100644 --- a/cmd/completion_util.go +++ b/cmd/completion_util.go @@ -16,9 +16,10 @@ import ( ) func CompleteFunctionList(cmd *cobra.Command, args []string, toComplete string) (strings []string, directive cobra.ShellCompDirective) { + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) listers := []fn.Lister{ - knative.NewLister(false), - k8s.NewLister(false), + knative.NewLister(kc, false), + k8s.NewLister(kc, false), } items := []fn.ListItem{} diff --git a/cmd/config.go b/cmd/config.go index 2dccd67560..6143d1ddd7 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -12,6 +12,7 @@ import ( "knative.dev/func/cmd/common" "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" ) func NewConfigCmd( @@ -89,14 +90,16 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) { if err != nil { return } + // construct k8s client for some commands do cluster search + kc := k8s.NewClient(k8s.BuildClientConfig(function.Deploy.Cluster, "", function.Local)) switch answers.SelectedOperation { case "Add": switch answers.SelectedConfig { case "Volumes": - err = runAddVolumesPrompt(cmd.Context(), function) + err = runAddVolumesPrompt(cmd.Context(), kc, function) case "Environment variables": - err = runAddEnvsPrompt(cmd.Context(), function) + err = runAddEnvsPrompt(cmd.Context(), kc, function) case "Labels": err = runAddLabelsPrompt(cmd.Context(), function, common.DefaultLoaderSaver) case "Git": diff --git a/cmd/config_envs.go b/cmd/config_envs.go index 4d03a0f347..ef784e366d 100644 --- a/cmd/config_envs.go +++ b/cmd/config_envs.go @@ -130,7 +130,8 @@ set environment variable from a secret return loadSaver.Save(function) } - return runAddEnvsPrompt(cmd.Context(), function) + kc := k8s.NewClient(k8s.BuildClientConfig(function.Deploy.Cluster, "", function.Local)) + return runAddEnvsPrompt(cmd.Context(), kc, function) }, } @@ -211,7 +212,7 @@ func listEnvs(f fn.Function, w io.Writer, outputFormat Format) error { } } -func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) { +func runAddEnvsPrompt(ctx context.Context, kc *k8s.Client, f fn.Function) (err error) { insertToIndex := 0 @@ -243,11 +244,11 @@ func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) { } // SECTION - select the type of Environment variable to be added - secrets, err := k8s.ListSecretsNamesIfConnected(ctx, f.Deploy.Namespace) + secrets, err := k8s.ListSecretsNamesIfConnected(ctx, kc, f.Deploy.Namespace) if err != nil { return } - configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, f.Deploy.Namespace) + configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, kc, f.Deploy.Namespace) if err != nil { return } diff --git a/cmd/config_volumes.go b/cmd/config_volumes.go index c0b3926351..ef5fdfaf0b 100644 --- a/cmd/config_volumes.go +++ b/cmd/config_volumes.go @@ -98,7 +98,8 @@ For non-interactive usage, use flags to specify the volume type and configuratio } // Fall back to interactive mode - return runAddVolumesPrompt(cmd.Context(), function) + kc := k8s.NewClient(k8s.BuildClientConfig(function.Deploy.Cluster, "", function.Local)) + return runAddVolumesPrompt(cmd.Context(), kc, function) }, } @@ -164,17 +165,17 @@ func listVolumes(f fn.Function) { } } -func runAddVolumesPrompt(ctx context.Context, f fn.Function) (err error) { +func runAddVolumesPrompt(ctx context.Context, kc *k8s.Client, f fn.Function) (err error) { - secrets, err := k8s.ListSecretsNamesIfConnected(ctx, f.Deploy.Namespace) + secrets, err := k8s.ListSecretsNamesIfConnected(ctx, kc, f.Deploy.Namespace) if err != nil { return } - configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, f.Deploy.Namespace) + configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, kc, f.Deploy.Namespace) if err != nil { return } - persistentVolumeClaims, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(ctx, f.Deploy.Namespace) + persistentVolumeClaims, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(ctx, kc, f.Deploy.Namespace) if err != nil { return } diff --git a/cmd/delete.go b/cmd/delete.go index 103a6270c4..47cea74d95 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -9,6 +9,7 @@ import ( "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" ) func NewDeleteCmd(newClient ClientFactory) *cobra.Command { @@ -33,7 +34,7 @@ No local files are deleted. SuggestFor: []string{"remove", "del"}, Aliases: []string{"rm"}, ValidArgsFunction: CompleteFunctionList, - PreRunE: bindEnv("path", "confirm", "all", "namespace", "verbose"), + PreRunE: bindEnv("cluster", "cluster-token", "path", "confirm", "all", "namespace", "verbose"), SilenceUsage: true, // no usage dump on error RunE: func(cmd *cobra.Command, args []string) error { // Layer 2: Catch technical errors and provide CLI-specific user-friendly messages @@ -47,7 +48,15 @@ No local files are deleted. fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) } + // Function Context + f, _ := fn.NewFunction(effectivePath()) + if f.Initialized() { + cfg = cfg.Apply(f) + } + // Flags + cmd.Flags().String("cluster", cfg.Cluster, "Specify a cluster api url for your function deployment. ($FUNC_CLUSTER)") + cmd.Flags().String("cluster-token", "", "Bearer token for cluster authentication. ($FUNC_CLUSTER_TOKEN)") cmd.Flags().StringP("namespace", "n", defaultNamespace(fn.Function{}, false), "The namespace when deleting by name. ($FUNC_NAMESPACE)") cmd.Flags().StringP("all", "a", "true", "Delete all resources created for a function, eg. Pipelines, Secrets, etc. ($FUNC_ALL) (allowed values: \"true\", \"false\")") addConfirmFlag(cmd, cfg.Confirm) @@ -78,26 +87,36 @@ func runDelete(cmd *cobra.Command, args []string, newClient ClientFactory) (err return } - client, done := newClient(ClientConfig{Verbose: cfg.Verbose}) - defer done() - if cfg.Name != "" { // Delete by name if provided + // don't use local.yaml auth because we are not concerned about func at path + kc := k8s.NewClient(k8s.BuildClientConfig(cfg.Cluster, cfg.ClusterToken, fn.Local{})) + + client, done := newClient(ClientConfig{Verbose: cfg.Verbose, K8sClient: kc}) + defer done() return client.Remove(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{}, cfg.All) - } else { // Otherwise; delete the function at path (cwd by default) - f, err := fn.NewFunction(cfg.Path) - if err != nil { - return err - } - return client.Remove(cmd.Context(), "", "", f, cfg.All) } + + // Delete by path + f, err := fn.NewFunction(cfg.Path) + if err != nil { + return err + } + + kc := k8s.NewClient(k8s.BuildClientConfig(cfg.Cluster, cfg.ClusterToken, f.Local)) + + client, done := newClient(ClientConfig{Verbose: cfg.Verbose, K8sClient: kc}) + defer done() + return client.Remove(cmd.Context(), "", "", f, cfg.All) } type deleteConfig struct { - Name string - Namespace string - Path string - All bool - Verbose bool + Cluster string + ClusterToken string + Name string + Namespace string + Path string + All bool + Verbose bool } // newDeleteConfig returns a config populated from the current execution context @@ -108,11 +127,13 @@ func newDeleteConfig(cmd *cobra.Command, args []string) (cfg deleteConfig, err e name = args[0] } cfg = deleteConfig{ - All: viper.GetBool("all"), - Name: name, // args[0] or derived - Namespace: viper.GetString("namespace"), - Path: viper.GetString("path"), - Verbose: viper.GetBool("verbose"), // defined on root + All: viper.GetBool("all"), + Cluster: viper.GetString("cluster"), + ClusterToken: viper.GetString("cluster-token"), + Name: name, // args[0] or derived + Namespace: viper.GetString("namespace"), + Path: viper.GetString("path"), + Verbose: viper.GetBool("verbose"), // defined on root } if cfg.Name == "" && cmd.Flags().Changed("namespace") { // logicially inconsistent to supply only a namespace. diff --git a/cmd/deploy.go b/cmd/deploy.go index e5a1a8af29..7d97cde855 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -130,7 +130,7 @@ EXAMPLES `, SuggestFor: []string{"delpoy", "deplyo"}, PreRunE: bindEnv("build", "build-timestamp", "builder", "builder-image", - "base-image", "confirm", "domain", "env", "git-branch", "git-dir", + "base-image", "cluster", "cluster-token", "confirm", "domain", "env", "git-branch", "git-dir", "git-url", "image", "image-pull-secret", "management-disabled", "namespace", "path", "platform", "push", "pvc-size", "service-account", "deployer", "registry", "registry-insecure", "registry-authfile", "remote", "username", "password", "token", "verbose", @@ -159,6 +159,8 @@ EXAMPLES // contextually relevant function; but sets are flattened via cfg.Apply(f) cmd.Flags().StringP("builder", "b", cfg.Builder, fmt.Sprintf("Builder to use when creating the function's container. Currently supported builders are %s.", KnownBuilders())) + cmd.Flags().String("cluster", cfg.Cluster, "Specify a cluster api url for your function deployment. ($FUNC_CLUSTER)") + cmd.Flags().String("cluster-token", "", "Bearer token for cluster authentication. Persisted to .func/local.yaml on successful deploy. ($FUNC_CLUSTER_TOKEN)") cmd.Flags().StringP("registry", "r", cfg.Registry, "Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. ($FUNC_REGISTRY)") cmd.Flags().Bool("registry-insecure", cfg.RegistryInsecure, "Skip TLS certificate verification when communicating in HTTPS with the registry. The value is persisted over consecutive runs ($FUNC_REGISTRY_INSECURE)") @@ -283,35 +285,24 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { // Warn if registry changed but registryInsecure is still true warnRegistryInsecureChange(cmd.OutOrStderr(), cfg.Registry, f) + // Warn when changing clusters for potential auth problems + warnClusterChange(cmd.OutOrStderr(), cfg.Cluster, f.Deploy.Cluster) if f, err = cfg.Configure(f); err != nil { // Updates f with deploy cfg return } - changingNamespace := func(f fn.Function) bool { - // We're changing namespace if: - return f.Deploy.Namespace != "" && // it's already deployed - f.Namespace != "" && // a specific (new) namespace is requested - (f.Namespace != f.Deploy.Namespace) // and it's different - } + // construct our k8s client via custom client config + kc := k8s.NewClient(k8s.BuildClientConfig(cfg.Cluster, cfg.ClusterToken, f.Local)) - // If we're changing namespace in an OpenShift cluster, we have to - // also update the registry because there is a registry per namespace, - // and their name includes the namespace. - // This saves needing a manual flag ``--registry={destination namespace registry}`` - if changingNamespace(f) && k8s.IsOpenShift() && k8s.IsOpenShiftInternalRegistry(f.Registry) { - f.Registry = "image-registry.openshift-image-registry.svc:5000/" + f.Namespace - if cfg.Verbose { - fmt.Fprintf(cmd.OutOrStdout(), "Info: Overriding openshift registry to %s\n", f.Registry) - } - } + f.Registry = resolveRegistry(f.Registry, f.Namespace, kc) // Informative non-error messages regarding the final deployment request - printDeployMessages(cmd.OutOrStdout(), f) + printDeployMessages(cmd.OutOrStdout(), f, kc) // Get options based on the value of the config such as concrete impls // of builders and pushers based on the value of the --builder flag - clientOptions, err := cfg.clientOptions() + clientOptions, err := cfg.clientOptions(kc) if err != nil { return } @@ -473,6 +464,10 @@ func KnownBuilders() builders.Known { type deployConfig struct { buildConfig // further embeds config.Global + // ClusterToken is a bearer token for authenticating to the deployment + // cluster. When set, it takes precedence over stored credentials. + ClusterToken string + // Perform build using the settings from the embedded buildConfig struct. // Acceptable values are the keyword 'auto', or a truthy value such as // 'true', 'false, '1' or '0'. @@ -567,6 +562,7 @@ type deployConfig struct { func newDeployConfig(cmd *cobra.Command) deployConfig { cfg := deployConfig{ buildConfig: newBuildConfig(), + ClusterToken: viper.GetString("cluster-token"), Build: viper.GetString("build"), Env: viper.GetStringSlice("env"), Domain: viper.GetString("domain"), @@ -790,19 +786,19 @@ func (c deployConfig) Validate(cmd *cobra.Command) (err error) { } // clientOptions returns client options specific to deploy, including the appropriate deployer -func (c deployConfig) clientOptions() ([]fn.Option, error) { +func (c deployConfig) clientOptions(kc *k8s.Client) ([]fn.Option, error) { // Start with build config options - o, err := c.buildConfig.clientOptions() + o, err := c.buildConfig.clientOptions(kc) if err != nil { return o, err } - t := newTransport(c.RegistryInsecure) - creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile, c.RegistryInsecure) + t := newTransport(c.RegistryInsecure, kc) + creds := newCredentialsProvider(config.Dir(), t, c.RegistryAuthfile, c.RegistryInsecure, kc) // Override the pipelines provider to use custom credentials // This is needed for remote builds (deploy --remote) - o = append(o, fn.WithPipelinesProvider(newTektonPipelinesProvider(creds, c.Verbose, t))) + o = append(o, fn.WithPipelinesProvider(newTektonPipelinesProvider(creds, c.Verbose, t, kc))) // Add the appropriate deployer based on deploy type deployer := c.Deployer @@ -812,11 +808,11 @@ func (c deployConfig) clientOptions() ([]fn.Option, error) { switch deployer { case knative.KnativeDeployerName: - o = append(o, fn.WithDeployer(newKnativeDeployer(c.Verbose))) + o = append(o, fn.WithDeployer(newKnativeDeployer(c.Verbose, kc))) case k8s.KubernetesDeployerName: - o = append(o, fn.WithDeployer(newK8sDeployer(c.Verbose))) + o = append(o, fn.WithDeployer(newK8sDeployer(c.Verbose, kc))) case keda.KedaDeployerName: - o = append(o, fn.WithDeployer(newKedaDeployer(c.Verbose))) + o = append(o, fn.WithDeployer(newKedaDeployer(c.Verbose, kc))) default: return o, fmt.Errorf("unsupported deploy type: %s (supported: %s, %s, %s)", deployer, knative.KnativeDeployerName, k8s.KubernetesDeployerName, keda.KedaDeployerName) } @@ -825,7 +821,7 @@ func (c deployConfig) clientOptions() ([]fn.Option, error) { } // printDeployMessages to the output. Non-error deployment messages. -func printDeployMessages(out io.Writer, f fn.Function) { +func printDeployMessages(out io.Writer, f fn.Function, cc *k8s.Client) { digest, err := isDigested(f.Image) if err == nil && digest { fmt.Fprintf(out, "Deploying image '%v', which has a digest. Build and push are disabled.\n", f.Image) @@ -850,7 +846,7 @@ func printDeployMessages(out io.Writer, f fn.Function) { // If the target namespace is provided but differs from active, warn because // the function won't be visible to other commands such as kubectl unless // context namespace is switched. - activeNamespace, err := k8s.GetDefaultNamespace() + activeNamespace, err := cc.DefaultNamespace() if err == nil && targetNamespace != "" && targetNamespace != activeNamespace { fmt.Fprintf(out, "Warning: namespace chosen is '%s', but currently active namespace is '%s'. Continuing with deployment to '%s'.\n", targetNamespace, activeNamespace, targetNamespace) } @@ -894,6 +890,24 @@ func printDeployMessages(out io.Writer, f fn.Function) { } } +// resolveRegistry returns the registry to use for the function. +// - If no registry is set, checks for OpenShift and uses its internal registry. +// - If an OpenShift internal registry is set and namespace is provided, +// rewrites the registry to include the target namespace. +// - Otherwise returns the registry as-is. +func resolveRegistry(registry, namespace string, c *k8s.Client) string { + if registry == "" { + if c.IsOpenshift() { + return k8s.GetDefaultOpenShiftRegistry(c) + } + return "" + } + if namespace != "" && c.IsOpenshift() && k8s.IsOpenShiftInternalRegistry(registry) { + return "image-registry.openshift-image-registry.svc:5000/" + namespace + } + return registry +} + // isDigested checks that the given image reference has a digest. Invalid // reference return error. func isDigested(v string) (validDigest bool, err error) { diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 540cb4e15f..b9e8b66cf4 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -1228,7 +1228,8 @@ func TestDeploy_NamespaceUpdateWarning(t *testing.T) { time.Sleep(1 * time.Second) - activeNamespace, err := k8s.GetDefaultNamespace() + defaultKc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + activeNamespace, err := defaultKc.DefaultNamespace() if err != nil { t.Fatalf("Couldn't get active namespace, got error: %v", err) } @@ -1341,71 +1342,29 @@ func TestDeploy_BasicRedeployPipelinesCorrectNamespace(t *testing.T) { // namespace on OpenShift does not overwrite an external registry (e.g. // docker.io/user) with the internal OpenShift registry. // Regression test for https://github.com/knative/func/issues/2172 -func TestDeploy_NamespaceChangePreservesExternalRegistry(t *testing.T) { - root := FromTempDirectory(t) - - cleanup := k8s.SetOpenShiftForTest(true) - defer cleanup() - - // Create a function deployed to "ns1" with an external registry - f := fn.Function{ - Runtime: "go", - Root: root, - Registry: "docker.io/user", - Deploy: fn.DeploySpec{Namespace: "ns1"}, - } - f, err := fn.New().Init(f) - if err != nil { - t.Fatal(err) - } - - // Deploy to a different namespace - cmd := NewDeployCmd(NewTestClient(fn.WithDeployer(mock.NewDeployer()))) - cmd.SetArgs([]string{"--namespace=ns2"}) - if err := cmd.Execute(); err != nil { - t.Fatal(err) - } - - // Reload and verify the external registry was preserved - f, _ = fn.NewFunction(root) - if f.Registry != "docker.io/user" { - t.Errorf("expected registry 'docker.io/user' to be preserved, got %q", f.Registry) - } -} - -// TestDeploy_NamespaceChangeUpdatesInternalRegistry ensures that changing -// namespace on OpenShift DOES update the registry when the function uses -// the internal OpenShift registry (the namespace is part of the registry path). -func TestDeploy_NamespaceChangeUpdatesInternalRegistry(t *testing.T) { - root := FromTempDirectory(t) - - cleanup := k8s.SetOpenShiftForTest(true) - defer cleanup() - - // Create a function deployed to "ns1" using the internal registry - f := fn.Function{ - Runtime: "go", - Root: root, - Registry: "image-registry.openshift-image-registry.svc:5000/ns1", - Deploy: fn.DeploySpec{Namespace: "ns1"}, - } - f, err := fn.New().Init(f) - if err != nil { - t.Fatal(err) - } +func TestResolveRegistry_OpenShift(t *testing.T) { + cc := k8s.NewClientWithOpenShift( + k8s.BuildClientConfig("fake-cluster", "", fn.Local{}), + true) - // Deploy to a different namespace - cmd := NewDeployCmd(NewTestClient(fn.WithDeployer(mock.NewDeployer()))) - cmd.SetArgs([]string{"--namespace=ns2"}) - if err := cmd.Execute(); err != nil { - t.Fatal(err) + tests := []struct { + name string + registry string + namespace string + expected string + }{ + {"external registry preserved", "docker.io/user", "ns2", "docker.io/user"}, + {"internal registry rewritten", "image-registry.openshift-image-registry.svc:5000/ns1", "ns2", "image-registry.openshift-image-registry.svc:5000/ns2"}, + {"empty fallback to openshift", "", "", "image-registry.openshift-image-registry.svc:5000/default"}, } - // Reload and verify the internal registry was updated to the new namespace - f, _ = fn.NewFunction(root) - expected := "image-registry.openshift-image-registry.svc:5000/ns2" - if f.Registry != expected { - t.Errorf("expected registry to update to %q, got %q", expected, f.Registry) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ret := resolveRegistry(tt.registry, tt.namespace, cc) + if ret != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, ret) + } + }) } } diff --git a/cmd/describe.go b/cmd/describe.go index 5a6f4a8d49..452ea7bb38 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -12,6 +12,7 @@ import ( "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" ) func NewDescribeCmd(newClient ClientFactory) *cobra.Command { @@ -34,7 +35,7 @@ the current directory or from the directory specified with --path. ValidArgsFunction: CompleteFunctionList, Aliases: []string{"info", "desc"}, - PreRunE: bindEnv("output", "path", "namespace", "verbose"), + PreRunE: bindEnv("cluster", "cluster-token", "output", "path", "namespace", "verbose"), RunE: func(cmd *cobra.Command, args []string) error { return runDescribe(cmd, args, newClient) }, @@ -46,7 +47,15 @@ the current directory or from the directory specified with --path. fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) } + // Function Context + f, _ := fn.NewFunction(effectivePath()) + if f.Initialized() { + cfg = cfg.Apply(f) + } + // Flags + cmd.Flags().String("cluster", cfg.Cluster, "Specify a cluster api url for your function deployment. ($FUNC_CLUSTER)") + cmd.Flags().String("cluster-token", "", "Bearer token for cluster authentication. ($FUNC_CLUSTER_TOKEN)") cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|yaml|url) ($FUNC_OUTPUT)") cmd.Flags().StringP("namespace", "n", defaultNamespace(fn.Function{}, false), "The namespace in which to look for the named function. ($FUNC_NAMESPACE)") addPathFlag(cmd) @@ -66,11 +75,13 @@ func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (er } // TODO cfg.Prompt() - client, done := newClient(ClientConfig{Verbose: cfg.Verbose}) - defer done() - var details fn.Instance if cfg.Name != "" { // Describe by name if provided + // don't use local.yaml auth because we are not concerned about func at path + kc := k8s.NewClient(k8s.BuildClientConfig(cfg.Cluster, cfg.ClusterToken, fn.Local{})) + + client, done := newClient(ClientConfig{Verbose: cfg.Verbose, K8sClient: kc}) + defer done() details, err = client.Describe(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{}) if err != nil { return err @@ -83,6 +94,11 @@ func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (er if !f.Initialized() { return NewErrNotInitializedFromPath(f.Root, "describe") } + + kc := k8s.NewClient(k8s.BuildClientConfig(cfg.Cluster, cfg.ClusterToken, f.Local)) + + client, done := newClient(ClientConfig{Verbose: cfg.Verbose, K8sClient: kc}) + defer done() details, err = client.Describe(cmd.Context(), "", "", f) if err != nil { return err @@ -97,11 +113,13 @@ func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (er // ------------------------------ type describeConfig struct { - Name string - Namespace string - Output string - Path string - Verbose bool + Cluster string + ClusterToken string + Name string + Namespace string + Output string + Path string + Verbose bool } func newDescribeConfig(cmd *cobra.Command, args []string) (cfg describeConfig, err error) { @@ -110,11 +128,13 @@ func newDescribeConfig(cmd *cobra.Command, args []string) (cfg describeConfig, e name = args[0] } cfg = describeConfig{ - Name: name, - Namespace: viper.GetString("namespace"), - Output: viper.GetString("output"), - Path: viper.GetString("path"), - Verbose: viper.GetBool("verbose"), + Cluster: viper.GetString("cluster"), + ClusterToken: viper.GetString("cluster-token"), + Name: name, + Namespace: viper.GetString("namespace"), + Output: viper.GetString("output"), + Path: viper.GetString("path"), + Verbose: viper.GetBool("verbose"), } if cfg.Name == "" && cmd.Flags().Changed("namespace") { // logically inconsistent to supply only a namespace. diff --git a/cmd/environment.go b/cmd/environment.go index 31aee04f31..0cef7be1d7 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -114,13 +114,16 @@ func runEnvironment(cmd *cobra.Command, newClient ClientFactory, v *Version) (er return } - // Gets the cluster host + f, _ := functions.NewFunction(cfg.Path) + + // Gets the cluster host — use the function's stored cluster if available var host string - cc, err := k8s.GetClientConfig().ClientConfig() + envKc := k8s.NewClient(k8s.BuildClientConfig(f.Deploy.Cluster, "", f.Local)) + restCfg, err := envKc.ClientConfig() if err != nil { fmt.Printf("error getting client config %v\n", err) } else { - host = cc.Host + host = restCfg.Host } //Get default image builders @@ -146,12 +149,11 @@ func runEnvironment(cmd *cobra.Command, newClient ClientFactory, v *Version) (er Defaults: defaults, } - function, instance := describeFuncInformation(cmd.Context(), newClient, cfg) - if function != nil { - environment.Function = function - } - if instance != nil { - environment.Instance = instance + if f.Initialized() { + environment.Function = &f + if instance := describeFuncInformation(cmd.Context(), f, envKc, newClient, cfg); instance != nil { + environment.Instance = instance + } } var s []byte @@ -191,20 +193,14 @@ func getTemplates(client *functions.Client, runtimes []string) (map[string][]str return templateMap, nil } -func describeFuncInformation(context context.Context, newClient ClientFactory, cfg environmentConfig) (*functions.Function, *functions.Instance) { - function, err := functions.NewFunction(cfg.Path) - if err != nil || !function.Initialized() { - return nil, nil - } - - client, done := newClient(ClientConfig{Verbose: cfg.Verbose}) +func describeFuncInformation(ctx context.Context, f functions.Function, kc *k8s.Client, newClient ClientFactory, cfg environmentConfig) *functions.Instance { + client, done := newClient(ClientConfig{Verbose: cfg.Verbose, K8sClient: kc}) defer done() - - instance, err := client.Describe(context, function.Name, function.Deploy.Namespace, function) + instance, err := client.Describe(ctx, f.Name, f.Deploy.Namespace, f) if err != nil { - return &function, nil + return nil } - return &function, &instance + return &instance } type environmentConfig struct { diff --git a/cmd/errors.go b/cmd/errors.go index 60d3361b4a..41da8a94f9 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -267,10 +267,11 @@ func (e *ErrClusterNotAccessible) Error() string { Cannot connect to Kubernetes cluster. No valid cluster configuration found. Try this: - minikube start Start Minikube cluster - kind create cluster Start Kind cluster - kubectl cluster-info Verify cluster is running - kubectl config get-contexts List available contexts + func deploy --cluster --cluster-token Connect directly without kubeconfig + minikube start Start Minikube cluster + kind create cluster Start Kind cluster + kubectl cluster-info Verify cluster is running + kubectl config get-contexts List available contexts For more options, run 'func deploy --help'`, e.Err) } // end if diff --git a/cmd/func-util/main.go b/cmd/func-util/main.go index 360fcf38a7..ca9b9d8550 100644 --- a/cmd/func-util/main.go +++ b/cmd/func-util/main.go @@ -156,21 +156,22 @@ func deploy(ctx context.Context) error { if f.Deploy.Deployer == "" { f.Deploy.Deployer = knative.KnativeDeployerName } + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) var d fn.Deployer switch f.Deploy.Deployer { case knative.KnativeDeployerName: - d = knative.NewDeployer( - knative.WithDeployerDecorator(deployDecorator{}), + d = knative.NewDeployer(kc, + knative.WithDeployerDecorator(deployDecorator{k8sClient: kc}), knative.WithDeployerVerbose(true), ) case k8s.KubernetesDeployerName: - d = k8s.NewDeployer( - k8s.WithDeployerDecorator(deployDecorator{}), + d = k8s.NewDeployer(kc, + k8s.WithDeployerDecorator(deployDecorator{k8sClient: kc}), k8s.WithDeployerVerbose(true), ) case keda.KedaDeployerName: - d = keda.NewDeployer( - keda.WithDeployerDecorator(deployDecorator{}), + d = keda.NewDeployer(kc, + keda.WithDeployerDecorator(deployDecorator{k8sClient: kc}), keda.WithDeployerVerbose(true), ) default: @@ -188,18 +189,19 @@ func deploy(ctx context.Context) error { } type deployDecorator struct { - oshDec k8s.OpenshiftMetadataDecorator + k8sClient *k8s.Client + oshDec k8s.OpenshiftMetadataDecorator } func (d deployDecorator) UpdateAnnotations(function fn.Function, annotations map[string]string) map[string]string { - if k8s.IsOpenShift() { + if d.k8sClient != nil && d.k8sClient.IsOpenshift() { return d.oshDec.UpdateAnnotations(function, annotations) } return annotations } func (d deployDecorator) UpdateLabels(function fn.Function, labels map[string]string) map[string]string { - if k8s.IsOpenShift() { + if d.k8sClient != nil && d.k8sClient.IsOpenshift() { return d.oshDec.UpdateLabels(function, labels) } return labels diff --git a/cmd/logs.go b/cmd/logs.go index 22783e0892..0792703498 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -13,6 +13,7 @@ import ( "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" ) @@ -130,7 +131,8 @@ func runLogs(cmd *cobra.Command, newClient ClientFactory) error { fmt.Fprintf(os.Stderr, "Streaming logs for function '%s' in namespace '%s'...\n", f.Name, f.Namespace) fmt.Fprintf(os.Stderr, "Press Ctrl+C to stop.\n\n") - err = knative.GetKServiceLogs(ctx, f.Namespace, f.Name, f.Image, sinceTime, os.Stdout) + kc := k8s.NewClient(k8s.BuildClientConfig(f.Deploy.Cluster, "", f.Local)) + err = knative.GetKServiceLogs(ctx, kc, f.Namespace, f.Name, f.Image, sinceTime, os.Stdout) if err != nil && err != context.Canceled { return fmt.Errorf("failed to stream logs: %w", err) } diff --git a/cmd/root.go b/cmd/root.go index df89eb7479..034c1f9658 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -137,16 +137,6 @@ Learn more about Knative at: https://knative.dev`, cfg.Name), // Helpers // ------------------------------------------ -// registry to use is that provided as --registry or FUNC_REGISTRY. -// If not provided, global configuration determines the default to use. -func registry() string { - if r := viper.GetString("registry"); r != "" { - return r - } - cfg, _ := config.NewDefault() - return cfg.RegistryDefault() -} - // effectivePath to use is that which was provided by --path or FUNC_PATH. // Manually parses flags such that this can be used during (cobra/viper) flag // definition (prior to parsing). @@ -189,7 +179,8 @@ func defaultNamespace(f fn.Function, verbose bool) string { } // Active K8S namespace - namespace, err := k8s.GetDefaultNamespace() + defaultKc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + namespace, err := defaultKc.DefaultNamespace() if err != nil { if verbose { fmt.Fprintf(os.Stderr, "Unable to get current active kubernetes namespace. Defaults will be used. %v", err) diff --git a/cmd/run.go b/cmd/run.go index bea2c961cd..ae7cbb67b8 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -16,6 +16,7 @@ import ( "knative.dev/func/pkg/config" "knative.dev/func/pkg/docker" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/oci" ) @@ -180,8 +181,10 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { cfg.Verbose = false } - // Client - clientOptions, err := cfg.clientOptions() + // construct a k8s client with default k8s configs because 'run' shares its + // function client options with build - buildConfig.clientOptions() + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + clientOptions, err := cfg.clientOptions(kc) if err != nil { return } diff --git a/docs/reference/func_delete.md b/docs/reference/func_delete.md index c22a87a692..52247bec9a 100644 --- a/docs/reference/func_delete.md +++ b/docs/reference/func_delete.md @@ -32,12 +32,14 @@ func delete myfunc --namespace apps ### Options ``` - -a, --all string Delete all resources created for a function, eg. Pipelines, Secrets, etc. ($FUNC_ALL) (allowed values: "true", "false") (default "true") - -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) - -h, --help help for delete - -n, --namespace string The namespace when deleting by name. ($FUNC_NAMESPACE) (default "default") - -p, --path string Path to the function. Default is current directory ($FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) + -a, --all string Delete all resources created for a function, eg. Pipelines, Secrets, etc. ($FUNC_ALL) (allowed values: "true", "false") (default "true") + --cluster string Specify a cluster api url for your function deployment. ($FUNC_CLUSTER) + --cluster-token string Bearer token for cluster authentication. ($FUNC_CLUSTER_TOKEN) + -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) + -h, --help help for delete + -n, --namespace string The namespace when deleting by name. ($FUNC_NAMESPACE) (default "default") + -p, --path string Path to the function. Default is current directory ($FUNC_PATH) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` ### SEE ALSO diff --git a/docs/reference/func_deploy.md b/docs/reference/func_deploy.md index 81880ce1c4..d36e59b1e2 100644 --- a/docs/reference/func_deploy.md +++ b/docs/reference/func_deploy.md @@ -118,6 +118,8 @@ func deploy --build-timestamp Use the actual time as the created time for the docker image. This is only useful for buildpacks builder. -b, --builder string Builder to use when creating the function's container. Currently supported builders are "host", "pack" and "s2i". (default "pack") --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) + --cluster string Specify a cluster api url for your function deployment. ($FUNC_CLUSTER) + --cluster-token string Bearer token for cluster authentication. Persisted to .func/local.yaml on successful deploy. ($FUNC_CLUSTER_TOKEN) -c, --confirm Prompt to confirm options interactively ($FUNC_CONFIRM) --deployer string Type of deployment to use: 'knative' for Knative Service (default), 'raw' for Kubernetes Deployment or 'keda' for Deployment with a Keda HTTP scaler ($FUNC_DEPLOY_TYPE) --domain string Domain to use for the function's route. Cluster must be configured with domain matching for the given domain (ignored if unrecognized) ($FUNC_DOMAIN) diff --git a/docs/reference/func_describe.md b/docs/reference/func_describe.md index 7e5254392b..1904a9ff7e 100644 --- a/docs/reference/func_describe.md +++ b/docs/reference/func_describe.md @@ -29,11 +29,13 @@ func describe --output yaml --path myotherfunc ### Options ``` - -h, --help help for describe - -n, --namespace string The namespace in which to look for the named function. ($FUNC_NAMESPACE) (default "default") - -o, --output string Output format (human|plain|json|yaml|url) ($FUNC_OUTPUT) (default "human") - -p, --path string Path to the function. Default is current directory ($FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) + --cluster string Specify a cluster api url for your function deployment. ($FUNC_CLUSTER) + --cluster-token string Bearer token for cluster authentication. ($FUNC_CLUSTER_TOKEN) + -h, --help help for describe + -n, --namespace string The namespace in which to look for the named function. ($FUNC_NAMESPACE) (default "default") + -o, --output string Output format (human|plain|json|yaml|url) ($FUNC_OUTPUT) (default "human") + -p, --path string Path to the function. Default is current directory ($FUNC_PATH) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` ### SEE ALSO diff --git a/e2e/e2e_core_test.go b/e2e/e2e_core_test.go index d2bae71d13..9aa125f9c4 100644 --- a/e2e/e2e_core_test.go +++ b/e2e/e2e_core_test.go @@ -12,6 +12,7 @@ import ( "testing" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" ) @@ -398,7 +399,8 @@ func TestCore_Delete(t *testing.T) { } // Check it appears in the list - client := fn.New(fn.WithListers(knative.NewLister(false))) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + client := fn.New(fn.WithListers(knative.NewLister(kc, false))) list, err := client.List(t.Context(), Namespace) if err != nil { t.Fatal(err) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index abbb2511e5..c69b76e42b 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -29,7 +29,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" "knative.dev/func/pkg/k8s" ) @@ -859,11 +858,8 @@ func isAbnormalExit(t *testing.T, err error) bool { func setSecret(t *testing.T, name, ns string, data map[string][]byte) { t.Helper() ctx := t.Context() - config, err := k8s.GetClientConfig().ClientConfig() - if err != nil { - t.Fatal(err) - } - clientset, err := kubernetes.NewForConfig(config) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + clientset, err := kc.Clientset() if err != nil { t.Fatal(err) } @@ -882,11 +878,8 @@ func setSecret(t *testing.T, name, ns string, data map[string][]byte) { func setConfigMap(t *testing.T, name, ns string, data map[string]string) { t.Helper() ctx := t.Context() - config, err := k8s.GetClientConfig().ClientConfig() - if err != nil { - t.Fatal(err) - } - clientset, err := kubernetes.NewForConfig(config) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + clientset, err := kc.Clientset() if err != nil { t.Fatal(err) } diff --git a/pkg/builders/builders_int_test.go b/pkg/builders/builders_int_test.go index a57fbd58c0..07d137e806 100644 --- a/pkg/builders/builders_int_test.go +++ b/pkg/builders/builders_int_test.go @@ -374,7 +374,7 @@ func servePrivateGit(ctx context.Context, t *testing.T, certDir string) { image = "ghcr.io/matejvasek/git-private:latest" ) - k8sClient, err := k8s.NewKubernetesClientset() + k8sClient, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } diff --git a/pkg/config/config.go b/pkg/config/config.go index 0b8dddd9e7..5ad1e35d0a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,7 +12,6 @@ import ( "gopkg.in/yaml.v2" "knative.dev/func/pkg/builders" fn "knative.dev/func/pkg/functions" - "knative.dev/func/pkg/k8s" ) const ( @@ -31,17 +30,18 @@ const ( // Global configuration settings. type Global struct { - Builder string `yaml:"builder,omitempty"` - Confirm bool `yaml:"confirm,omitempty"` - Language string `yaml:"language,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - Registry string `yaml:"registry,omitempty"` - Verbose bool `yaml:"verbose,omitempty"` + Builder string `yaml:"builder,omitempty"` + Confirm bool `yaml:"confirm,omitempty"` + Language string `yaml:"language,omitempty"` + Namespace string `yaml:"namespace,omitempty"` + Registry string `yaml:"registry,omitempty"` + Verbose bool `yaml:"verbose,omitempty"` + RegistryInsecure bool `yaml:"registryInsecure,omitempty"` + Cluster string `yaml:"cluster,omitempty"` + // NOTE: all members must include their yaml serialized names, even when // this is the default, because these tag values are used for the static // getter/setter accessors to match requests. - - RegistryInsecure bool `yaml:"registryInsecure,omitempty"` } // New Config struct with all members set to static defaults. See NewDefaults @@ -54,22 +54,6 @@ func New() Global { } } -// RegistyDefault is a convenience method for deferred calculation of a -// default registry taking into account both the global config file and cluster -// detection. -func (c Global) RegistryDefault() string { - // If defined, the user's choice for global registry default value is used - if c.Registry != "" { - return c.Registry - } - switch { - case k8s.IsOpenShift(): - return k8s.GetDefaultOpenShiftRegistry() - default: - return "" - } -} - // NewDefault returns a config populated by global defaults as defined by the // config file located in .Path() (the global func settings path, which is // @@ -141,6 +125,9 @@ func (c Global) Apply(f fn.Function) Global { // Unconditional because bool has no "empty value". Works because // viper resolves the correct precedence via our defaulting. c.RegistryInsecure = f.RegistryInsecure + if f.Deploy.Cluster != "" { + c.Cluster = f.Deploy.Cluster + } return c } @@ -164,6 +151,9 @@ func (c Global) Configure(f fn.Function) fn.Function { // viper resolves the correct precedence via our defaulting. f.RegistryInsecure = c.RegistryInsecure + // Unconditional to allow --cluster= (empty value) to use kubeconfig context + f.Deploy.Cluster = c.Cluster + return f } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1e3dd287b8..d8a0d6f655 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -372,6 +372,7 @@ func TestList(t *testing.T) { values := config.List() expected := []string{ "builder", + "cluster", "confirm", "language", "namespace", diff --git a/pkg/deployer/testing/integration_test_helper.go b/pkg/deployer/testing/integration_test_helper.go index 379cad6a73..abe72f6ce5 100644 --- a/pkg/deployer/testing/integration_test_helper.go +++ b/pkg/deployer/testing/integration_test_helper.go @@ -458,7 +458,7 @@ func TestInt_Scale(t *testing.T, deployer fn.Deployer, remover fn.Remover, descr // Check the actual number of pods running using Kubernetes API // This is much more reliable than checking logs - cliSet, err := k8s.NewKubernetesClientset() + cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } @@ -608,7 +608,7 @@ func TestInt_EnvsUpdate(t *testing.T, deployer fn.Deployer, remover fn.Remover, t.Fatal(err) } - cliSet, err := k8s.NewKubernetesClientset() + cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } @@ -663,7 +663,7 @@ func TestInt_FullPath(t *testing.T, deployer fn.Deployer, remover fn.Remover, li ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) t.Cleanup(cancel) - cliSet, err := k8s.NewKubernetesClientset() + cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } @@ -759,7 +759,7 @@ func TestInt_FullPath(t *testing.T, deployer fn.Deployer, remover fn.Remover, li buff := new(k8s.SynchronizedBuffer) go func() { selector := fmt.Sprintf("function.knative.dev/name=%s", functionName) - _ = k8s.GetPodLogsBySelector(ctx, namespace, selector, "user-container", "", &now, buff) + _ = k8s.GetPodLogsBySelector(ctx, k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})), namespace, selector, "user-container", "", &now, buff) }() depRes, err := deployer.Deploy(ctx, function) @@ -839,7 +839,7 @@ func TestInt_FullPath(t *testing.T, deployer fn.Deployer, remover fn.Remover, li redeployLogBuff := new(k8s.SynchronizedBuffer) go func() { selector := fmt.Sprintf("function.knative.dev/name=%s", functionName) - _ = k8s.GetPodLogsBySelector(ctx, namespace, selector, "user-container", "", &now, redeployLogBuff) + _ = k8s.GetPodLogsBySelector(ctx, k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})), namespace, selector, "user-container", "", &now, redeployLogBuff) }() _, err = deployer.Deploy(ctx, function) @@ -1040,7 +1040,7 @@ func createTrigger(t *testing.T, ctx context.Context, namespace, triggerName str }, }, } - eventingClient, err := knative.NewEventingClient(namespace) + eventingClient, err := knative.NewEventingClient(k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})), namespace) if err != nil { t.Fatal(err) } @@ -1073,7 +1073,7 @@ func createTrigger(t *testing.T, ctx context.Context, namespace, triggerName str func createSecret(t *testing.T, namespace, name string, data map[string]string) { t.Helper() - cliSet, err := k8s.NewKubernetesClientset() + cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } @@ -1104,7 +1104,7 @@ func createSecret(t *testing.T, namespace, name string, data map[string]string) func createConfigMap(t *testing.T, namespace, name string, data map[string]string) { t.Helper() - cliSet, err := k8s.NewKubernetesClientset() + cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } @@ -1131,19 +1131,19 @@ func deferCleanup(t *testing.T, namespace string, resourceType string, name stri switch resourceType { case "secret": t.Cleanup(func() { - if cliSet, err := k8s.NewKubernetesClientset(); err == nil { + if cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset(); err == nil { _ = cliSet.CoreV1().Secrets(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) } }) case "configmap": t.Cleanup(func() { - if cliSet, err := k8s.NewKubernetesClientset(); err == nil { + if cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset(); err == nil { _ = cliSet.CoreV1().ConfigMaps(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) } }) case "trigger": t.Cleanup(func() { - if eventingClient, err := knative.NewEventingClient(namespace); err == nil { + if eventingClient, err := knative.NewEventingClient(k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})), namespace); err == nil { _ = eventingClient.DeleteTrigger(context.Background(), name) } }) @@ -1224,7 +1224,7 @@ func TestInt_OperatorSync(t *testing.T, deployer fn.Deployer, remover fn.Remover fn.WithDeployer(deployer), fn.WithDescribers(describer), fn.WithRemovers(remover), - fn.WithSyncer(operator.NewSyncer()), + fn.WithSyncer(operator.NewSyncer(k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})))), ) f, err := client.Init(fn.Function{ @@ -1270,7 +1270,7 @@ func TestInt_OperatorSync(t *testing.T, deployer fn.Deployer, remover fn.Remover }) // Verify CR state only if the Function CRD is installed - restCfg, err := k8s.GetClientConfig().ClientConfig() + restCfg, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).ClientConfig() if err != nil { t.Fatalf("getting kubernetes config: %v", err) } @@ -1394,8 +1394,8 @@ func getHttpClient(ctx context.Context, deployer string) (*http.Client, func(), case k8s.KubernetesDeployerName, keda.KedaDeployerName: // For Kubernetes deployments, use in-cluster dialer to access ClusterIP services - clientConfig := k8s.GetClientConfig() - dialer, err := k8s.NewInClusterDialer(ctx, clientConfig) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + dialer, err := k8s.NewInClusterDialer(ctx, kc) if err != nil { return nil, noopDeferFunc, fmt.Errorf("failed to create in-cluster dialer: %w", err) } diff --git a/pkg/functions/client_int_test.go b/pkg/functions/client_int_test.go index b474e57621..63c50a44bc 100644 --- a/pkg/functions/client_int_test.go +++ b/pkg/functions/client_int_test.go @@ -212,7 +212,8 @@ func TestInt_Update_WithAnnotationsAndLabels(t *testing.T) { functionName := "updateannlab" verbose := false - servingClient, err := knative.NewServingClient(DefaultIntTestNamespace) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + servingClient, err := knative.NewServingClient(kc, DefaultIntTestNamespace) if err != nil { t.Fatal(err) } @@ -666,22 +667,24 @@ func resetEnv() { // newClient creates an instance of the func client with concrete impls // sufficient for running integration tests. func newClient(verbose bool) *fn.Client { + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) return fn.New( fn.WithRegistry(DefaultIntTestRegistry), fn.WithRegistryInsecure(true), fn.WithScaffolder(oci.NewScaffolder(true)), fn.WithBuilder(oci.NewBuilder("", verbose)), fn.WithPusher(oci.NewPusher(true, true, verbose)), - fn.WithDeployer(knative.NewDeployer(knative.WithDeployerVerbose(verbose))), - fn.WithDescribers(knative.NewDescriber(verbose), k8s.NewDescriber(verbose)), - fn.WithRemovers(knative.NewRemover(verbose), k8s.NewRemover(verbose)), - fn.WithListers(knative.NewLister(verbose), k8s.NewLister(verbose)), + fn.WithDeployer(knative.NewDeployer(kc, knative.WithDeployerVerbose(verbose))), + fn.WithDescribers(knative.NewDescriber(kc, verbose), k8s.NewDescriber(kc, verbose)), + fn.WithRemovers(knative.NewRemover(kc, verbose), k8s.NewRemover(kc, verbose)), + fn.WithListers(knative.NewLister(kc, verbose), k8s.NewLister(kc, verbose)), fn.WithVerbose(verbose), ) } // copy of newClient just with s2i methods instead func newClientWithS2i(verbose bool) *fn.Client { + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) return fn.New( fn.WithRegistry(DefaultIntTestRegistry), fn.WithRegistryInsecure(true), @@ -689,10 +692,10 @@ func newClientWithS2i(verbose bool) *fn.Client { fn.WithScaffolder(s2i.NewScaffolder(true)), fn.WithBuilder(s2i.NewBuilder(s2i.WithVerbose(verbose))), fn.WithPusher(docker.NewPusher(docker.WithVerbose(verbose), docker.WithInsecure(true))), - fn.WithDeployer(knative.NewDeployer(knative.WithDeployerVerbose(verbose))), - fn.WithDescribers(knative.NewDescriber(verbose), k8s.NewDescriber(verbose)), - fn.WithRemovers(knative.NewRemover(verbose), k8s.NewRemover(verbose)), - fn.WithListers(knative.NewLister(verbose), k8s.NewLister(verbose)), + fn.WithDeployer(knative.NewDeployer(kc, knative.WithDeployerVerbose(verbose))), + fn.WithDescribers(knative.NewDescriber(kc, verbose), k8s.NewDescriber(kc, verbose)), + fn.WithRemovers(knative.NewRemover(kc, verbose), k8s.NewRemover(kc, verbose)), + fn.WithListers(knative.NewLister(kc, verbose), k8s.NewLister(kc, verbose)), ) } diff --git a/pkg/functions/function.go b/pkg/functions/function.go index ff07f41225..7a42367cd2 100644 --- a/pkg/functions/function.go +++ b/pkg/functions/function.go @@ -48,6 +48,73 @@ type Local struct { // Remote indicates the deployment (and possibly build) process are to // be triggered in a remote environment rather than run locally. Remote bool `yaml:"remote,omitempty"` + + // Auth holds per-cluster authentication entries + Auth []AuthEntry `yaml:"auth,omitempty"` +} + +// AuthEntry is the complete structure for verification on cluster side and +// authentication on user side for cluster to deploy to. +type AuthEntry struct { + ClusterURL string `yaml:"cluster-url"` + Cluster ClusterVerify `yaml:"cluster,omitempty"` + User UserAuth `yaml:"user,omitempty"` +} + +type ClusterVerify struct { + CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"` + CertificateAuthority string `yaml:"certificate-authority,omitempty"` + InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"` +} + +// UserAuth holds user credentials for authenticating to a cluster. +type UserAuth struct { + ClientCertificateData string `yaml:"client-certificate-data,omitempty"` + ClientKeyData string `yaml:"client-key-data,omitempty"` + ClientCertificate string `yaml:"client-certificate,omitempty"` + ClientKey string `yaml:"client-key,omitempty"` + Token string `yaml:"token,omitempty"` + Exec *ExecAuth `yaml:"exec,omitempty"` +} + +type ExecAuth struct { + Command string `yaml:"command"` + Args []string `yaml:"args,omitempty"` + Env []ExecEnv `yaml:"env,omitempty"` + APIVersion string `yaml:"apiVersion,omitempty"` +} + +type ExecEnv struct { + Name string `yaml:"name"` + Value string `yaml:"value"` +} + +// FindAuth returns first AuthEntry matching the cluster URL it finds, or nil +// if no entry matches. +func (l Local) FindAuth(url string) *AuthEntry { + for i := range l.Auth { + if l.Auth[i].ClusterURL == url { + return &l.Auth[i] + } + } + return nil +} + +// SetAuth upserts an auth entry for the given cluster URL. If an entry with the +// same URL already exists it is updated; otherwise a new entry is appended. +func (l *Local) SetAuth(clusterURL string, cluster ClusterVerify, user UserAuth) { + for i := range l.Auth { + if l.Auth[i].ClusterURL == clusterURL { + l.Auth[i].Cluster = cluster + l.Auth[i].User = user + return + } + } + l.Auth = append(l.Auth, AuthEntry{ + ClusterURL: clusterURL, + Cluster: cluster, + User: user, + }) } // Function @@ -196,6 +263,9 @@ type DeploySpec struct { // Namespace into which the function was deployed on supported platforms. Namespace string `yaml:"namespace,omitempty"` + // Cluster is the cluster server api URL where the function is deployed + Cluster string `yaml:"cluster,omitempty"` + // Image is the deployed image including sha256 Image string `yaml:"image,omitempty"` diff --git a/pkg/http/openshift.go b/pkg/http/openshift.go index 0d9b5d8939..410b385167 100644 --- a/pkg/http/openshift.go +++ b/pkg/http/openshift.go @@ -13,15 +13,15 @@ import ( const openShiftRegistryHost = "image-registry.openshift-image-registry.svc" // WithOpenShiftServiceCA enables trust to OpenShift's service CA for internal image registry -func WithOpenShiftServiceCA() Option { +func WithOpenShiftServiceCA(c *k8s.Client) Option { var err error var ca *x509.Certificate var o sync.Once selectCA := func(ctx context.Context, serverName string) (*x509.Certificate, error) { - if strings.HasPrefix(serverName, openShiftRegistryHost) { + if c != nil && strings.HasPrefix(serverName, openShiftRegistryHost) { o.Do(func() { - ca, err = k8s.GetOpenShiftServiceCA(ctx) + ca, err = c.GetOpenShiftServiceCA(ctx) if err != nil { err = fmt.Errorf("cannot get CA: %w", err) } diff --git a/pkg/http/openshift_int_test.go b/pkg/http/openshift_int_test.go index 2cc6c5e30c..28a82195d6 100644 --- a/pkg/http/openshift_int_test.go +++ b/pkg/http/openshift_int_test.go @@ -6,17 +6,19 @@ import ( "net/http" "testing" + fn "knative.dev/func/pkg/functions" fnhttp "knative.dev/func/pkg/http" "knative.dev/func/pkg/k8s" ) func TestInt_RoundTripper(t *testing.T) { - if !k8s.IsOpenShift() { + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + if !kc.IsOpenshift() { t.Skip("The cluster in not an instance of OpenShift.") return } - transport := fnhttp.NewRoundTripper(fnhttp.WithOpenShiftServiceCA()) + transport := fnhttp.NewRoundTripper(kc, fnhttp.WithOpenShiftServiceCA(kc)) defer transport.Close() client := http.Client{ diff --git a/pkg/http/transport.go b/pkg/http/transport.go index 6424d766e0..df18a2fbaa 100644 --- a/pkg/http/transport.go +++ b/pkg/http/transport.go @@ -54,11 +54,13 @@ func WithInsecureSkipVerify(insecureSkipVerify bool) Option { // if the dial operation fails due to hostname resolution the RoundTripper tries to dial from in cluster pod. // // This is useful for accessing cluster internal services (pushing a CloudEvent into Knative broker). -func NewRoundTripper(opts ...Option) RoundTripCloser { +func NewRoundTripper(kc *k8s.Client, opts ...Option) RoundTripCloser { o := options{ - inClusterDialer: k8s.NewLazyInitInClusterDialer(k8s.GetClientConfig()), insecureSkipVerify: false, } + if kc != nil { + o.inClusterDialer = k8s.NewLazyInitInClusterDialer(kc) + } for _, option := range opts { option(&o) } @@ -133,6 +135,9 @@ func (d *dialerWithFallback) DialContext(ctx context.Context, network, address s return nil, err } + if d.fallbackDialer == nil { + return nil, err + } return d.fallbackDialer.DialContext(ctx, network, address) } @@ -145,7 +150,9 @@ func (d *dialerWithFallback) Close() error { errs = append(errs, err) } - err = d.fallbackDialer.Close() + if d.fallbackDialer != nil { + err = d.fallbackDialer.Close() + } if err != nil { errs = append(errs, err) } diff --git a/pkg/http/transport_test.go b/pkg/http/transport_test.go index ee6b9e744f..8d02db76b4 100644 --- a/pkg/http/transport_test.go +++ b/pkg/http/transport_test.go @@ -39,7 +39,7 @@ func TestCustomCA(t *testing.T) { backingAddr: inClusterAddr, } - tr := fnhttp.NewRoundTripper( + tr := fnhttp.NewRoundTripper(nil, fnhttp.WithSelectCA(mockSelectCA), fnhttp.WithInClusterDialer(mockInCusterDialer)) defer tr.Close() diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index 8d528280df..ce6d6ad321 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -1,13 +1,19 @@ package k8s import ( + "encoding/base64" "fmt" + "sync" "time" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + fn "knative.dev/func/pkg/functions" ) const ( @@ -15,45 +21,125 @@ const ( DefaultErrorWindowTimeout = 2 * time.Second ) -func NewClientAndResolvedNamespace(ns string) (*kubernetes.Clientset, string, error) { +// Client wraps a clientcmd.ClientConfig and provides convenience methods +// for creating Kubernetes clients. All cluster access in the codebase should +// go through a Client instance rather than calling package-level functions. +type Client struct { + cc clientcmd.ClientConfig + openshift bool + openShiftOnce sync.Once +} + +// NewClient creates a Client from the given ClientConfig. +func NewClient(cc clientcmd.ClientConfig) *Client { + return &Client{cc: cc} +} + +// NewClientWithOpenShift creates a Client with a pre-set OpenShift detection +// result. For tests only. +func NewClientWithOpenShift(cc clientcmd.ClientConfig, isOpenShift bool) *Client { + c := &Client{cc: cc, openshift: isOpenShift} + c.openShiftOnce.Do(func() {}) // prevent real detection + return c +} + +// ClientConfig returns the underlying rest.Config. +func (c *Client) ClientConfig() (*rest.Config, error) { + return c.cc.ClientConfig() +} + +// Clientset creates a new kubernetes.Clientset. +func (c *Client) Clientset() (*kubernetes.Clientset, error) { + cfg, err := c.cc.ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to create new kubernetes client: %w", err) + } + return kubernetes.NewForConfig(cfg) +} + +// ClientAndNamespace creates a Clientset and resolves the namespace, +// falling back to the default namespace from the config if ns is empty. +func (c *Client) ClientAndNamespace(ns string) (*kubernetes.Clientset, string, error) { var err error if ns == "" { - ns, err = GetDefaultNamespace() + ns, err = c.DefaultNamespace() if err != nil { return nil, ns, err } } - - client, err := NewKubernetesClientset() + client, err := c.Clientset() return client, ns, err } -func NewKubernetesClientset() (*kubernetes.Clientset, error) { - restConfig, err := GetClientConfig().ClientConfig() +// DynamicClient creates a new dynamic.Interface. +func (c *Client) DynamicClient() (dynamic.Interface, error) { + cfg, err := c.cc.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to create new kubernetes client: %w", err) } - - return kubernetes.NewForConfig(restConfig) + return dynamic.NewForConfig(cfg) } -func NewDynamicClient() (dynamic.Interface, error) { - restConfig, err := GetClientConfig().ClientConfig() - if err != nil { - return nil, fmt.Errorf("failed to create new kubernetes client: %w", err) - } - - return dynamic.NewForConfig(restConfig) +// DefaultNamespace returns the default namespace from the config. +func (c *Client) DefaultNamespace() (string, error) { + ns, _, err := c.cc.Namespace() + return ns, err } -// GetDefaultNamespace returns default namespace -func GetDefaultNamespace() (namespace string, err error) { - namespace, _, err = GetClientConfig().Namespace() - return +// RawConfig returns the raw kubeconfig API config. +func (c *Client) RawConfig() (clientcmdapi.Config, error) { + return c.cc.RawConfig() } -func GetClientConfig() clientcmd.ClientConfig { +// BuildClientConfig creates the k8s client config with possible overrides to +// the cluster url or its auth fields. Base64-encoded string fields from +// local.yaml are decoded to []byte for the clientcmd API. +func BuildClientConfig(url string, token string, local fn.Local) clientcmd.ClientConfig { + co := clientcmd.ConfigOverrides{} + if url != "" { + co.ClusterInfo.Server = url + if entry := local.FindAuth(url); entry != nil { + // cluster verification data + co.ClusterInfo.CertificateAuthorityData = decodeBase64(entry.Cluster.CertificateAuthorityData) + co.ClusterInfo.CertificateAuthority = entry.Cluster.CertificateAuthority + co.ClusterInfo.InsecureSkipTLSVerify = entry.Cluster.InsecureSkipTLSVerify + // user authentication data + co.AuthInfo.ClientCertificateData = decodeBase64(entry.User.ClientCertificateData) + co.AuthInfo.ClientCertificate = entry.User.ClientCertificate + co.AuthInfo.ClientKeyData = decodeBase64(entry.User.ClientKeyData) + co.AuthInfo.ClientKey = entry.User.ClientKey + co.AuthInfo.Token = entry.User.Token + if entry.User.Exec != nil { + co.AuthInfo.Exec = &clientcmdapi.ExecConfig{ + Command: entry.User.Exec.Command, + Args: entry.User.Exec.Args, + APIVersion: entry.User.Exec.APIVersion, + } + for _, e := range entry.User.Exec.Env { + co.AuthInfo.Exec.Env = append(co.AuthInfo.Exec.Env, clientcmdapi.ExecEnvVar{ + Name: e.Name, + Value: e.Value, + }) + } + } + } + } + // explicit flag '--token' has preference over stored values + if token != "" { + co.AuthInfo.Token = token + } return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( clientcmd.NewDefaultClientConfigLoadingRules(), - &clientcmd.ConfigOverrides{}) + &co) +} + +func decodeBase64(s string) []byte { + if s == "" { + return nil + } + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil + } + return b } diff --git a/pkg/k8s/client_test.go b/pkg/k8s/client_test.go new file mode 100644 index 0000000000..eefef25925 --- /dev/null +++ b/pkg/k8s/client_test.go @@ -0,0 +1,522 @@ +package k8s + +import ( + "encoding/base64" + "os" + "path/filepath" + "testing" + + "gopkg.in/yaml.v2" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + fn "knative.dev/func/pkg/functions" +) + +// writeTestKubeconfig serializes a clientcmdapi.Config to a temp file and +// points the KUBECONFIG env var at it for the duration of the test. +func writeTestKubeconfig(t *testing.T, cfg clientcmdapi.Config) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "kubeconfig") + if err := clientcmd.WriteToFile(cfg, path); err != nil { + t.Fatalf("failed to write test kubeconfig: %v", err) + } + t.Setenv("KUBECONFIG", path) + return path +} + +func testKubeconfig() clientcmdapi.Config { + return clientcmdapi.Config{ + CurrentContext: "test-ctx", + Contexts: map[string]*clientcmdapi.Context{ + "test-ctx": {Cluster: "test-cluster", AuthInfo: "test-user", Namespace: "test-ns"}, + }, + Clusters: map[string]*clientcmdapi.Cluster{ + "test-cluster": { + Server: "https://kubeconfig.example.com:6443", + CertificateAuthorityData: []byte("-----BEGIN CERTIFICATE-----\nCA-DATA\n-----END CERTIFICATE-----"), + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "test-user": { + Token: "kubeconfig-token", + }, + }, + } +} + +func testKubeconfigWithCerts() clientcmdapi.Config { + return clientcmdapi.Config{ + CurrentContext: "cert-ctx", + Contexts: map[string]*clientcmdapi.Context{ + "cert-ctx": {Cluster: "cert-cluster", AuthInfo: "cert-user", Namespace: "cert-ns"}, + }, + Clusters: map[string]*clientcmdapi.Cluster{ + "cert-cluster": { + Server: "https://cert.example.com:6443", + CertificateAuthorityData: []byte("-----BEGIN CERTIFICATE-----\nCA-PEM\n-----END CERTIFICATE-----"), + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "cert-user": { + ClientCertificateData: []byte("-----BEGIN CERTIFICATE-----\nCLIENT-CERT-PEM\n-----END CERTIFICATE-----"), + ClientKeyData: []byte("-----BEGIN RSA PRIVATE KEY-----\nCLIENT-KEY-PEM\n-----END RSA PRIVATE KEY-----"), + }, + }, + } +} + +// --- BuildClientConfig tests --- + +func TestBuildClientConfig_NoOverride_UsesKubeconfig(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + cc := BuildClientConfig("", "", fn.Local{}) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.Host != "https://kubeconfig.example.com:6443" { + t.Fatalf("expected kubeconfig host, got %s", cfg.Host) + } + if cfg.BearerToken != "kubeconfig-token" { + t.Fatalf("expected kubeconfig token, got %s", cfg.BearerToken) + } +} + +func TestBuildClientConfig_NoOverride_Namespace(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + cc := BuildClientConfig("", "", fn.Local{}) + ns, _, err := cc.Namespace() + if err != nil { + t.Fatal(err) + } + if ns != "test-ns" { + t.Fatalf("expected test-ns, got %s", ns) + } +} + +func TestBuildClientConfig_ClusterOverride(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + cc := BuildClientConfig("https://override.example.com:6443", "", fn.Local{}) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.Host != "https://override.example.com:6443" { + t.Fatalf("expected override host, got %s", cfg.Host) + } +} + +func TestBuildClientConfig_TokenOverride(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + cc := BuildClientConfig("https://override.example.com:6443", "my-token", fn.Local{}) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.BearerToken != "my-token" { + t.Fatalf("expected my-token, got %s", cfg.BearerToken) + } +} + +func TestBuildClientConfig_TokenWithoutCluster_AppliedToKubeconfig(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + cc := BuildClientConfig("", "override-token", fn.Local{}) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.BearerToken != "override-token" { + t.Fatalf("expected override-token to apply even without cluster URL, got %s", cfg.BearerToken) + } + if cfg.Host != "https://kubeconfig.example.com:6443" { + t.Fatalf("expected kubeconfig host unchanged, got %s", cfg.Host) + } +} + +func TestBuildClientConfig_StoredAuth_Token(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + local := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://stored.example.com:6443", + User: fn.UserAuth{Token: "stored-token"}, + }}, + } + + cc := BuildClientConfig("https://stored.example.com:6443", "", local) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.Host != "https://stored.example.com:6443" { + t.Fatalf("expected stored host, got %s", cfg.Host) + } + if cfg.BearerToken != "stored-token" { + t.Fatalf("expected stored-token, got %s", cfg.BearerToken) + } +} + +func TestBuildClientConfig_StoredAuth_CertData(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + caRaw := []byte("-----BEGIN CERTIFICATE-----\nMY-CA\n-----END CERTIFICATE-----") + certRaw := []byte("-----BEGIN CERTIFICATE-----\nMY-CERT\n-----END CERTIFICATE-----") + keyRaw := []byte("-----BEGIN RSA PRIVATE KEY-----\nMY-KEY\n-----END RSA PRIVATE KEY-----") + + local := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://certs.example.com", + Cluster: fn.ClusterVerify{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(caRaw), + }, + User: fn.UserAuth{ + ClientCertificateData: base64.StdEncoding.EncodeToString(certRaw), + ClientKeyData: base64.StdEncoding.EncodeToString(keyRaw), + }, + }}, + } + + cc := BuildClientConfig("https://certs.example.com", "", local) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if string(cfg.CAData) != string(caRaw) { + t.Fatalf("CAData mismatch: got %q", cfg.CAData) + } + if string(cfg.CertData) != string(certRaw) { + t.Fatalf("CertData mismatch: got %q", cfg.CertData) + } + if string(cfg.KeyData) != string(keyRaw) { + t.Fatalf("KeyData mismatch: got %q", cfg.KeyData) + } +} + +func TestBuildClientConfig_StoredAuth_InsecureSkipTLS(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + local := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://insecure.example.com", + Cluster: fn.ClusterVerify{ + InsecureSkipTLSVerify: true, + }, + User: fn.UserAuth{Token: "tok"}, + }}, + } + + cc := BuildClientConfig("https://insecure.example.com", "", local) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if !cfg.Insecure { + t.Fatal("expected Insecure=true") + } +} + +func TestBuildClientConfig_StoredAuth_CertFilePaths(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + dir := t.TempDir() + caFile := filepath.Join(dir, "ca.crt") + certFile := filepath.Join(dir, "client.crt") + keyFile := filepath.Join(dir, "client.key") + + if err := os.WriteFile(caFile, []byte("CA-FILE-DATA"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(certFile, []byte("CERT-FILE-DATA"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(keyFile, []byte("KEY-FILE-DATA"), 0600); err != nil { + t.Fatal(err) + } + + local := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://filepaths.example.com", + Cluster: fn.ClusterVerify{ + CertificateAuthority: caFile, + }, + User: fn.UserAuth{ + ClientCertificate: certFile, + ClientKey: keyFile, + Token: "file-tok", + }, + }}, + } + + cc := BuildClientConfig("https://filepaths.example.com", "", local) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.CAFile != caFile { + t.Fatalf("expected CAFile %s, got %s", caFile, cfg.CAFile) + } + if cfg.CertFile != certFile { + t.Fatalf("expected CertFile %s, got %s", certFile, cfg.CertFile) + } + if cfg.KeyFile != keyFile { + t.Fatalf("expected KeyFile %s, got %s", keyFile, cfg.KeyFile) + } +} + +func TestBuildClientConfig_TokenOverride_BeatsStoredToken(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + local := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://stored.example.com", + User: fn.UserAuth{Token: "stored-token"}, + }}, + } + + cc := BuildClientConfig("https://stored.example.com", "flag-token", local) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.BearerToken != "flag-token" { + t.Fatalf("expected flag-token to win, got %s", cfg.BearerToken) + } +} + +func TestBuildClientConfig_ClusterOverride_BeatsKubeconfig(t *testing.T) { + writeTestKubeconfig(t, testKubeconfig()) + + cc := BuildClientConfig("https://override.example.com", "", fn.Local{}) + cfg, err := cc.ClientConfig() + if err != nil { + t.Fatal(err) + } + if cfg.Host != "https://override.example.com" { + t.Fatalf("expected override to beat kubeconfig, got %s", cfg.Host) + } +} + +// --- YAML round-trip tests for []byte fields --- + +func TestAuthEntry_YAMLRoundTrip(t *testing.T) { + caB64 := base64.StdEncoding.EncodeToString([]byte("CA-DATA")) + certB64 := base64.StdEncoding.EncodeToString([]byte("CLIENT-CERT")) + keyB64 := base64.StdEncoding.EncodeToString([]byte("CLIENT-KEY")) + + original := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://roundtrip.example.com", + Cluster: fn.ClusterVerify{ + CertificateAuthorityData: caB64, + InsecureSkipTLSVerify: true, + }, + User: fn.UserAuth{ + ClientCertificateData: certB64, + ClientKeyData: keyB64, + Token: "my-token", + }, + }}, + } + + // Marshal to YAML + data, err := yaml.Marshal(&original) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + // Verify the base64 string is present in YAML output + yamlStr := string(data) + if !contains(yamlStr, caB64) { + t.Fatalf("expected base64 CA data in YAML output, got:\n%s", yamlStr) + } + + // Unmarshal back + var restored fn.Local + if err := yaml.Unmarshal(data, &restored); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + if len(restored.Auth) != 1 { + t.Fatalf("expected 1 auth entry, got %d", len(restored.Auth)) + } + entry := restored.Auth[0] + + if entry.ClusterURL != "https://roundtrip.example.com" { + t.Fatalf("ClusterURL mismatch: %s", entry.ClusterURL) + } + if entry.Cluster.CertificateAuthorityData != caB64 { + t.Fatalf("CertificateAuthorityData mismatch: got %q", entry.Cluster.CertificateAuthorityData) + } + if !entry.Cluster.InsecureSkipTLSVerify { + t.Fatal("expected InsecureSkipTLSVerify=true") + } + if entry.User.ClientCertificateData != certB64 { + t.Fatalf("ClientCertificateData mismatch: got %q", entry.User.ClientCertificateData) + } + if entry.User.ClientKeyData != keyB64 { + t.Fatalf("ClientKeyData mismatch: got %q", entry.User.ClientKeyData) + } + if entry.User.Token != "my-token" { + t.Fatalf("Token mismatch: got %s", entry.User.Token) + } +} + +func TestAuthEntry_YAMLRoundTrip_WithFilePaths(t *testing.T) { + original := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://paths.example.com", + Cluster: fn.ClusterVerify{ + CertificateAuthority: "/path/to/ca.crt", + }, + User: fn.UserAuth{ + ClientCertificate: "/path/to/client.crt", + ClientKey: "/path/to/client.key", + Token: "path-token", + }, + }}, + } + + data, err := yaml.Marshal(&original) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var restored fn.Local + if err := yaml.Unmarshal(data, &restored); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + entry := restored.Auth[0] + if entry.Cluster.CertificateAuthority != "/path/to/ca.crt" { + t.Fatalf("CertificateAuthority mismatch: %s", entry.Cluster.CertificateAuthority) + } + if entry.User.ClientCertificate != "/path/to/client.crt" { + t.Fatalf("ClientCertificate mismatch: %s", entry.User.ClientCertificate) + } + if entry.User.ClientKey != "/path/to/client.key" { + t.Fatalf("ClientKey mismatch: %s", entry.User.ClientKey) + } +} + +func TestAuthEntry_YAMLRoundTrip_ExecAuth(t *testing.T) { + original := fn.Local{ + Auth: []fn.AuthEntry{{ + ClusterURL: "https://exec.example.com", + User: fn.UserAuth{ + Exec: &fn.ExecAuth{ + Command: "gke-gcloud-auth-plugin", + Args: []string{"--region", "us-central1"}, + APIVersion: "client.authentication.k8s.io/v1beta1", + Env: []fn.ExecEnv{ + {Name: "USE_GKE_GCLOUD_AUTH_PLUGIN", Value: "True"}, + }, + }, + }, + }}, + } + + data, err := yaml.Marshal(&original) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + + var restored fn.Local + if err := yaml.Unmarshal(data, &restored); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + + exec := restored.Auth[0].User.Exec + if exec == nil { + t.Fatal("expected Exec to be set") + } + if exec.Command != "gke-gcloud-auth-plugin" { + t.Fatalf("Command mismatch: %s", exec.Command) + } + if exec.APIVersion != "client.authentication.k8s.io/v1beta1" { + t.Fatalf("APIVersion mismatch: %s", exec.APIVersion) + } + if len(exec.Args) != 2 || exec.Args[0] != "--region" { + t.Fatalf("Args mismatch: %v", exec.Args) + } + if len(exec.Env) != 1 || exec.Env[0].Name != "USE_GKE_GCLOUD_AUTH_PLUGIN" { + t.Fatalf("Env mismatch: %v", exec.Env) + } +} + +// --- Kubeconfig-to-Local conversion test --- + +func TestKubeconfigToLocal_CertDataRoundTrip(t *testing.T) { + kubecfg := testKubeconfigWithCerts() + writeTestKubeconfig(t, kubecfg) + + // Load kubeconfig the same way BuildClientConfig does + cc := BuildClientConfig("", "", fn.Local{}) + raw, err := cc.RawConfig() + if err != nil { + t.Fatal(err) + } + + // Extract into our types (simulating what ExtractClusterAuth would do) + ctx := raw.Contexts[raw.CurrentContext] + cluster := raw.Clusters[ctx.Cluster] + authInfo := raw.AuthInfos[ctx.AuthInfo] + + // Encode kubeconfig []byte → base64 string (the boundary conversion) + entry := fn.AuthEntry{ + ClusterURL: cluster.Server, + Cluster: fn.ClusterVerify{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(cluster.CertificateAuthorityData), + }, + User: fn.UserAuth{ + ClientCertificateData: base64.StdEncoding.EncodeToString(authInfo.ClientCertificateData), + ClientKeyData: base64.StdEncoding.EncodeToString(authInfo.ClientKeyData), + }, + } + + // Verify the base64 decodes back to what we put in + decoded, _ := base64.StdEncoding.DecodeString(entry.Cluster.CertificateAuthorityData) + if string(decoded) != "-----BEGIN CERTIFICATE-----\nCA-PEM\n-----END CERTIFICATE-----" { + t.Fatalf("CA data mismatch: %q", decoded) + } + + // Now feed back into BuildClientConfig and verify it produces matching rest.Config + local := fn.Local{Auth: []fn.AuthEntry{entry}} + cc2 := BuildClientConfig(cluster.Server, "", local) + restCfg, err := cc2.ClientConfig() + if err != nil { + t.Fatal(err) + } + + if restCfg.Host != cluster.Server { + t.Fatalf("host mismatch: got %s", restCfg.Host) + } + if string(restCfg.CAData) != string(cluster.CertificateAuthorityData) { + t.Fatalf("CAData mismatch after round-trip") + } + if string(restCfg.CertData) != string(authInfo.ClientCertificateData) { + t.Fatalf("CertData mismatch after round-trip") + } + if string(restCfg.KeyData) != string(authInfo.ClientKeyData) { + t.Fatalf("KeyData mismatch after round-trip") + } +} + +func contains(s, substr string) bool { + return len(s) > 0 && len(substr) > 0 && stringContains(s, substr) +} + +func stringContains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/k8s/configmaps.go b/pkg/k8s/configmaps.go index abf88e0767..a2d834171c 100644 --- a/pkg/k8s/configmaps.go +++ b/pkg/k8s/configmaps.go @@ -12,8 +12,8 @@ import ( k8sclientcmd "k8s.io/client-go/tools/clientcmd" ) -func GetConfigMap(ctx context.Context, name, namespaceOverride string) (*corev1.ConfigMap, error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func GetConfigMap(ctx context.Context, c *Client, name, namespaceOverride string) (*corev1.ConfigMap, error) { + client, namespace, err := c.ClientAndNamespace(namespaceOverride) if err != nil { return nil, err } @@ -23,8 +23,8 @@ func GetConfigMap(ctx context.Context, name, namespaceOverride string) (*corev1. // ListConfigMapsNamesIfConnected lists names of ConfigMaps present and the current k8s context // returns empty list, if not connected to any cluster -func ListConfigMapsNamesIfConnected(ctx context.Context, namespaceOverride string) (names []string, err error) { - names, err = listConfigMapsNames(ctx, namespaceOverride) +func ListConfigMapsNamesIfConnected(ctx context.Context, c *Client, namespaceOverride string) (names []string, err error) { + names, err = listConfigMapsNames(ctx, c, namespaceOverride) if err != nil { // not logged our authorized to access resources if k8serrors.IsForbidden(err) || k8serrors.IsUnauthorized(err) || k8serrors.IsInvalid(err) || k8serrors.IsTimeout(err) { @@ -53,8 +53,8 @@ func ListConfigMapsNamesIfConnected(ctx context.Context, namespaceOverride strin return } -func listConfigMapsNames(ctx context.Context, namespaceOverride string) (names []string, err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func listConfigMapsNames(ctx context.Context, c *Client, namespaceOverride string) (names []string, err error) { + client, namespace, err := c.ClientAndNamespace(namespaceOverride) if err != nil { return } diff --git a/pkg/k8s/configmaps_test.go b/pkg/k8s/configmaps_test.go index 338163e9e0..b982b44cf4 100644 --- a/pkg/k8s/configmaps_test.go +++ b/pkg/k8s/configmaps_test.go @@ -3,12 +3,14 @@ package k8s_test import ( "testing" + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) func TestListConfigMapsNamesIfConnectedWrongKubeconfig(t *testing.T) { t.Setenv("KUBECONFIG", "/tmp/non-existent.config") - _, err := k8s.ListConfigMapsNamesIfConnected(t.Context(), "") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + _, err := k8s.ListConfigMapsNamesIfConnected(t.Context(), kc, "") if err != nil { t.Fatal(err) } @@ -16,7 +18,8 @@ func TestListConfigMapsNamesIfConnectedWrongKubeconfig(t *testing.T) { func TestListConfigMapsNamesIfConnectedWrongKubernentesMaster(t *testing.T) { t.Setenv("KUBERNETES_MASTER", "/tmp/non-existent.config") - _, err := k8s.ListConfigMapsNamesIfConnected(t.Context(), "") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + _, err := k8s.ListConfigMapsNamesIfConnected(t.Context(), kc, "") if err != nil { t.Fatal(err) } diff --git a/pkg/k8s/deployer.go b/pkg/k8s/deployer.go index 1341515ac8..f87c83cbc0 100644 --- a/pkg/k8s/deployer.go +++ b/pkg/k8s/deployer.go @@ -47,10 +47,11 @@ type DeployerOpt func(*Deployer) type Deployer struct { verbose bool decorator deployer.DeployDecorator + kc *Client } -func NewDeployer(opts ...DeployerOpt) *Deployer { - d := &Deployer{} +func NewDeployer(kc *Client, opts ...DeployerOpt) *Deployer { + d := &Deployer{kc: kc} for _, opt := range opts { opt(d) } @@ -69,20 +70,6 @@ func WithDeployerDecorator(decorator deployer.DeployDecorator) DeployerOpt { } } -func onClusterFix(f fn.Function) fn.Function { - // This only exists because of a bootstrapping problem with On-Cluster - // builds: It appears that, when sending a function to be built on-cluster - // the target namespace is not being transmitted in the pipeline - // configuration. We should figure out how to transmit this information - // to the pipeline run for initial builds. This is a new problem because - // earlier versions of this logic relied entirely on the current - // kubernetes context. - if f.Namespace == "" && f.Deploy.Namespace == "" { - f.Namespace, _ = GetDefaultNamespace() - } - return f -} - // newEventingClient creates a Knative Eventing client from a REST config func newEventingClient(config *rest.Config, namespace string) (clienteventingv1.KnEventingClient, error) { eventingClient, err := eventingv1client.NewForConfig(config) @@ -93,7 +80,6 @@ func newEventingClient(config *rest.Config, namespace string) (clienteventingv1. } func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResult, error) { - f = onClusterFix(f) // Choosing f.Namespace vs f.Deploy.Namespace: // This is minimal logic currently required of all deployer impls. // If f.Namespace is defined, this is the (possibly new) target @@ -122,7 +108,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu } // Get the Kubernetes REST config - config, err := GetClientConfig().ClientConfig() + config, err := d.kc.ClientConfig() if err != nil { return fn.DeploymentResult{}, err } @@ -156,7 +142,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu return fn.DeploymentResult{}, fmt.Errorf("failed to generate deployment resources: %w", err) } - if err = CheckResourcesArePresent(ctx, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret); err != nil { + if err = CheckResourcesArePresent(ctx, d.kc, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret); err != nil { return fn.DeploymentResult{}, fmt.Errorf("failed to validate referenced resources: %w", err) } @@ -205,7 +191,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu return fn.DeploymentResult{}, fmt.Errorf("failed to generate deployment resources: %w", err) } - if err = CheckResourcesArePresent(ctx, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret); err != nil { + if err = CheckResourcesArePresent(ctx, d.kc, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret); err != nil { return fn.DeploymentResult{}, fmt.Errorf("failed to validate referenced resources: %w", err) } @@ -490,10 +476,10 @@ func (d *Deployer) generateService(f fn.Function, namespace string, daprInstalle // CheckResourcesArePresent returns error if Secrets or ConfigMaps // referenced in input sets are not deployed on the cluster in the specified namespace -func CheckResourcesArePresent(ctx context.Context, namespace string, referencedSecrets, referencedConfigMaps, referencedPVCs *sets.Set[string], referencedServiceAccount, imagePullSecret string) error { +func CheckResourcesArePresent(ctx context.Context, kc *Client, namespace string, referencedSecrets, referencedConfigMaps, referencedPVCs *sets.Set[string], referencedServiceAccount, imagePullSecret string) error { errMsg := "" for s := range *referencedSecrets { - _, err := GetSecret(ctx, s, namespace) + _, err := GetSecret(ctx, kc, s, namespace) if err != nil { if errors.IsForbidden(err) { errMsg += " Ensure that the service account has the necessary permissions to access the secret.\n" @@ -504,14 +490,14 @@ func CheckResourcesArePresent(ctx context.Context, namespace string, referencedS } for cm := range *referencedConfigMaps { - _, err := GetConfigMap(ctx, cm, namespace) + _, err := GetConfigMap(ctx, kc, cm, namespace) if err != nil { errMsg += fmt.Sprintf(" referenced ConfigMap \"%s\" is not present in namespace \"%s\"\n", cm, namespace) } } for pvc := range *referencedPVCs { - _, err := GetPersistentVolumeClaim(ctx, pvc, namespace) + _, err := GetPersistentVolumeClaim(ctx, kc, pvc, namespace) if err != nil { errMsg += fmt.Sprintf(" referenced PersistentVolumeClaim \"%s\" is not present in namespace \"%s\"\n", pvc, namespace) } @@ -519,14 +505,14 @@ func CheckResourcesArePresent(ctx context.Context, namespace string, referencedS // check if referenced ServiceAccount is present in the namespace if it is not default if referencedServiceAccount != "" && referencedServiceAccount != "default" { - err := GetServiceAccount(ctx, referencedServiceAccount, namespace) + err := GetServiceAccount(ctx, kc, referencedServiceAccount, namespace) if err != nil { errMsg += fmt.Sprintf(" referenced ServiceAccount \"%s\" is not present in namespace \"%s\"\n", referencedServiceAccount, namespace) } } if imagePullSecret != "" { - _, err := GetSecret(ctx, imagePullSecret, namespace) + _, err := GetSecret(ctx, kc, imagePullSecret, namespace) if err != nil { errMsg += fmt.Sprintf(" referenced image pull Secret \"%s\" is not present in namespace \"%s\"\n", imagePullSecret, namespace) } diff --git a/pkg/k8s/deployer_int_test.go b/pkg/k8s/deployer_int_test.go index 57dac06983..0349d0d42e 100644 --- a/pkg/k8s/deployer_int_test.go +++ b/pkg/k8s/deployer_int_test.go @@ -6,72 +6,85 @@ import ( "testing" deployertesting "knative.dev/func/pkg/deployer/testing" + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) +func defaultKc() *k8s.Client { + return k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) +} + func TestInt_FullPath(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_FullPath(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewLister(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewLister(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_Deploy(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Deploy(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_Metadata(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Metadata(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_Events(t *testing.T) { t.Skip("Kubernetes deploy does not support func subscribe yet") + kc := defaultKc() deployertesting.TestInt_Events(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_Scale(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Scale(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_EnvsUpdate(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_EnvsUpdate(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_ResourceValidationOnFirstDeploy(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_ResourceValidationOnFirstDeploy(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } func TestInt_OperatorSync(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_OperatorSync(t, - k8s.NewDeployer(k8s.WithDeployerVerbose(false)), - k8s.NewRemover(false), - k8s.NewDescriber(false), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(false)), + k8s.NewRemover(kc, false), + k8s.NewDescriber(kc, false), k8s.KubernetesDeployerName) } diff --git a/pkg/k8s/describer.go b/pkg/k8s/describer.go index 14f8468fdf..53a594966a 100644 --- a/pkg/k8s/describer.go +++ b/pkg/k8s/describer.go @@ -16,6 +16,7 @@ import ( type Describer struct { verbose bool transport http.RoundTripper + kc *Client } type DescriberOpt func(*Describer) @@ -26,8 +27,8 @@ func WithDescriberTransport(transport http.RoundTripper) DescriberOpt { } } -func NewDescriber(verbose bool, opts ...DescriberOpt) *Describer { - d := &Describer{verbose: verbose} +func NewDescriber(kc *Client, verbose bool, opts ...DescriberOpt) *Describer { + d := &Describer{kc: kc, verbose: verbose} for _, o := range opts { o(d) } @@ -40,7 +41,7 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In return fn.Instance{}, fmt.Errorf("function namespace is required when describing %q", name) } - clientset, err := NewKubernetesClientset() + clientset, err := d.kc.Clientset() if err != nil { return fn.Instance{}, fmt.Errorf("unable to create k8s client: %v", err) } diff --git a/pkg/k8s/describer_int_test.go b/pkg/k8s/describer_int_test.go index 2643b41359..901ba89a66 100644 --- a/pkg/k8s/describer_int_test.go +++ b/pkg/k8s/describer_int_test.go @@ -10,9 +10,10 @@ import ( ) func TestInt_Describe(t *testing.T) { + kc := defaultKc() describertesting.TestInt_Describe(t, - k8s.NewDescriber(true), - k8s.NewDeployer(k8s.WithDeployerVerbose(true)), - k8s.NewRemover(true), + k8s.NewDescriber(kc, true), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(true)), + k8s.NewRemover(kc, true), k8s.KubernetesDeployerName) } diff --git a/pkg/k8s/dialer.go b/pkg/k8s/dialer.go index 5fe559c794..433eee9e4a 100644 --- a/pkg/k8s/dialer.go +++ b/pkg/k8s/dialer.go @@ -24,7 +24,6 @@ import ( "k8s.io/client-go/kubernetes/scheme" v1 "k8s.io/client-go/kubernetes/typed/core/v1" restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/remotecommand" ) @@ -48,9 +47,9 @@ var SocatImage = "ghcr.io/knative/func-utils:v2" // var client = http.Client{ // Transport: transport, // } -func NewInClusterDialer(ctx context.Context, clientConfig clientcmd.ClientConfig) (*contextDialer, error) { +func NewInClusterDialer(ctx context.Context, kc *Client) (*contextDialer, error) { c := &contextDialer{ - clientConfig: clientConfig, + configClient: kc, detachChan: make(chan struct{}), } err := c.startDialerPod(ctx) @@ -62,7 +61,7 @@ func NewInClusterDialer(ctx context.Context, clientConfig clientcmd.ClientConfig type contextDialer struct { coreV1 v1.CoreV1Interface - clientConfig clientcmd.ClientConfig + configClient *Client restConf *restclient.Config podName string namespace string @@ -252,7 +251,7 @@ func (c *contextDialer) Close() error { } func (c *contextDialer) startDialerPod(ctx context.Context) (err error) { - c.restConf, err = c.clientConfig.ClientConfig() + c.restConf, err = c.configClient.ClientConfig() if err != nil { return } @@ -269,7 +268,7 @@ func (c *contextDialer) startDialerPod(ctx context.Context) (err error) { } c.coreV1 = client.CoreV1() - c.namespace, _, err = c.clientConfig.Namespace() + c.namespace, err = c.configClient.DefaultNamespace() if err != nil { return } @@ -291,7 +290,7 @@ func (c *contextDialer) startDialerPod(ctx context.Context) (err error) { Annotations: nil, }, Spec: coreV1.PodSpec{ - SecurityContext: defaultPodSecurityContext(), + SecurityContext: defaultPodSecurityContext(c.configClient), Containers: []coreV1.Container{ { Name: c.podName, @@ -308,7 +307,7 @@ func (c *contextDialer) startDialerPod(ctx context.Context) (err error) { } creatOpts := metaV1.CreateOptions{} - ready := podReady(ctx, c.coreV1, c.podName, c.namespace) + ready := podReady(ctx, c.configClient, c.coreV1, c.podName, c.namespace) _, err = pods.Create(ctx, pod, creatOpts) if err != nil { @@ -401,7 +400,7 @@ func attach(ctx context.Context, restClient restclient.Interface, restConf *rest }) } -func podReady(ctx context.Context, core v1.CoreV1Interface, podName, namespace string) (errChan <-chan error) { +func podReady(ctx context.Context, kc *Client, core v1.CoreV1Interface, podName, namespace string) (errChan <-chan error) { d := make(chan error) errChan = d @@ -433,7 +432,7 @@ func podReady(ctx context.Context, core v1.CoreV1Interface, podName, namespace s return } if status.State.Terminated != nil { - msg, _ := GetPodLogs(ctx, namespace, podName, podName) + msg, _ := GetPodLogs(ctx, kc, namespace, podName, podName) d <- fmt.Errorf("pod prematurely exited (output: %q, exitcode: %d)", msg, status.State.Terminated.ExitCode) return } @@ -555,14 +554,14 @@ func newConn() (*io.PipeReader, *io.PipeWriter, *conn) { return pr1, pw0, rwc } -func NewLazyInitInClusterDialer(clientConfig clientcmd.ClientConfig) *lazyInitInClusterDialer { +func NewLazyInitInClusterDialer(kc *Client) *lazyInitInClusterDialer { return &lazyInitInClusterDialer{ - clientConfig: clientConfig, + kc: kc, } } type lazyInitInClusterDialer struct { - clientConfig clientcmd.ClientConfig + kc *Client contextDialer *contextDialer initErr error o sync.Once @@ -570,7 +569,7 @@ type lazyInitInClusterDialer struct { func (l *lazyInitInClusterDialer) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) { l.o.Do(func() { - l.contextDialer, l.initErr = NewInClusterDialer(ctx, l.clientConfig) + l.contextDialer, l.initErr = NewInClusterDialer(ctx, l.kc) }) if l.initErr != nil { return nil, l.initErr diff --git a/pkg/k8s/dialer_int_test.go b/pkg/k8s/dialer_int_test.go index b2b40dd36c..5c418e0ba3 100644 --- a/pkg/k8s/dialer_int_test.go +++ b/pkg/k8s/dialer_int_test.go @@ -20,7 +20,8 @@ import ( metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/client-go/kubernetes" + + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) @@ -35,14 +36,10 @@ func TestInt_DialInClusterService(t *testing.T) { var ctx = t.Context() // Initialize client configuration from kubeconfig or in-cluster config - clientConfig := k8s.GetClientConfig() + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) - // Extract the REST config and create a clientset for API operations - rc, err := clientConfig.ClientConfig() - if err != nil { - t.Fatal(err) - } - cliSet, err := kubernetes.NewForConfig(rc) + // Create a clientset for API operations + cliSet, err := kc.Clientset() if err != nil { t.Fatal(err) } @@ -56,7 +53,7 @@ func TestInt_DialInClusterService(t *testing.T) { } // Determine which namespace to use for test resources - testingNS, _, err := clientConfig.Namespace() + testingNS, err := kc.DefaultNamespace() if err != nil { t.Fatal(err) } @@ -157,7 +154,7 @@ func TestInt_DialInClusterService(t *testing.T) { // Initialize the InClusterDialer. This will create a socat pod in the // cluster that acts as a TCP proxy, allowing us to reach cluster-internal // services. The "lazy init" variant only creates the pod when first used. - dialer := k8s.NewLazyInitInClusterDialer(clientConfig) + dialer := k8s.NewLazyInitInClusterDialer(kc) t.Cleanup(func() { dialer.Close() }) @@ -222,7 +219,8 @@ func TestInt_DialInClusterService(t *testing.T) { func TestInt_DialUnreachable(t *testing.T) { var ctx = t.Context() - dialer, err := k8s.NewInClusterDialer(ctx, k8s.GetClientConfig()) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + dialer, err := k8s.NewInClusterDialer(ctx, kc) if err != nil { t.Fatal(err) } @@ -261,12 +259,8 @@ func TestInt_DialUnreachable(t *testing.T) { func TestInt_DialContextExpiry(t *testing.T) { var setupCtx = t.Context() - clientConfig := k8s.GetClientConfig() - rc, err := clientConfig.ClientConfig() - if err != nil { - t.Fatal(err) - } - cliSet, err := kubernetes.NewForConfig(rc) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + cliSet, err := kc.Clientset() if err != nil { t.Fatal(err) } @@ -275,7 +269,7 @@ func TestInt_DialContextExpiry(t *testing.T) { creatOpts := metaV1.CreateOptions{} deleteOpts := metaV1.DeleteOptions{PropagationPolicy: &pp} - testingNS, _, err := clientConfig.Namespace() + testingNS, err := kc.DefaultNamespace() if err != nil { t.Fatal(err) } @@ -326,7 +320,7 @@ func TestInt_DialContextExpiry(t *testing.T) { } // Create the dialer pod eagerly so that pod creation is not tied to dialCtx. - dialer, err := k8s.NewInClusterDialer(setupCtx, clientConfig) + dialer, err := k8s.NewInClusterDialer(setupCtx, kc) if err != nil { t.Fatal(err) } diff --git a/pkg/k8s/lister.go b/pkg/k8s/lister.go index 82e0566f88..7ceaffa589 100644 --- a/pkg/k8s/lister.go +++ b/pkg/k8s/lister.go @@ -13,16 +13,18 @@ import ( type Lister struct { verbose bool + kc *Client } -func NewLister(verbose bool) fn.Lister { +func NewLister(kc *Client, verbose bool) fn.Lister { return &Lister{ + kc: kc, verbose: verbose, } } func (l *Lister) List(ctx context.Context, namespace string) ([]fn.ListItem, error) { - clientset, err := NewKubernetesClientset() + clientset, err := l.kc.Clientset() if err != nil { return nil, fmt.Errorf("unable to create k8s client: %v", err) } diff --git a/pkg/k8s/lister_int_test.go b/pkg/k8s/lister_int_test.go index 10feed5a48..04171875f5 100644 --- a/pkg/k8s/lister_int_test.go +++ b/pkg/k8s/lister_int_test.go @@ -10,10 +10,11 @@ import ( ) func TestInt_List(t *testing.T) { + kc := defaultKc() listertesting.TestInt_List(t, - k8s.NewLister(true), - k8s.NewDeployer(k8s.WithDeployerVerbose(true)), - k8s.NewDescriber(true), - k8s.NewRemover(true), + k8s.NewLister(kc, true), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(true)), + k8s.NewDescriber(kc, true), + k8s.NewRemover(kc, true), k8s.KubernetesDeployerName) } diff --git a/pkg/k8s/logs.go b/pkg/k8s/logs.go index ed06caa1b1..03806eff80 100644 --- a/pkg/k8s/logs.go +++ b/pkg/k8s/logs.go @@ -16,13 +16,13 @@ import ( // GetPodLogs returns logs from a specified Container in a Pod, if container is empty string, // then the first container in the pod is selected. -func GetPodLogs(ctx context.Context, namespace, podName, containerName string) (string, error) { +func GetPodLogs(ctx context.Context, kc *Client, namespace, podName, containerName string) (string, error) { podLogOpts := corev1.PodLogOptions{} if containerName != "" { podLogOpts.Container = containerName } - client, namespace, _ := NewClientAndResolvedNamespace(namespace) + client, namespace, _ := kc.ClientAndNamespace(namespace) request := client.CoreV1().Pods(namespace).GetLogs(podName, &podLogOpts) containerLogStream, err := request.Stream(ctx) @@ -46,8 +46,8 @@ func GetPodLogs(ctx context.Context, namespace, podName, containerName string) ( // In addition, filtering on image can be done so only logs for given image are logged. // // This function runs as long as the passed context is active (i.e. it is required cancel the context to stop log gathering). -func GetPodLogsBySelector(ctx context.Context, namespace, labelSelector, containerName, image string, since *time.Time, out io.Writer) error { - client, namespace, err := NewClientAndResolvedNamespace(namespace) +func GetPodLogsBySelector(ctx context.Context, kc *Client, namespace, labelSelector, containerName, image string, since *time.Time, out io.Writer) error { + client, namespace, err := kc.ClientAndNamespace(namespace) if err != nil { return fmt.Errorf("cannot create k8s client: %w", err) } diff --git a/pkg/k8s/logs_int_test.go b/pkg/k8s/logs_int_test.go index d8b1b77181..8886c78f3a 100644 --- a/pkg/k8s/logs_int_test.go +++ b/pkg/k8s/logs_int_test.go @@ -11,6 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) @@ -18,7 +19,8 @@ func TestInt_GetPodLogs(t *testing.T) { var err error ctx, cancel := context.WithTimeout(t.Context(), time.Minute*5) t.Cleanup(cancel) - cliSet, err := k8s.NewKubernetesClientset() + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + cliSet, err := kc.Clientset() if err != nil { t.Fatal(err) } @@ -80,7 +82,7 @@ out: time.Sleep(time.Millisecond * 500) } - out, err := k8s.GetPodLogs(ctx, testingNS, testingPodName, testingPodName) + out, err := k8s.GetPodLogs(ctx, kc, testingNS, testingPodName, testingPodName) if err != nil { t.Fatal(err) } diff --git a/pkg/k8s/manifestival.go b/pkg/k8s/manifestival.go index 7909d4f84a..334f029c84 100644 --- a/pkg/k8s/manifestival.go +++ b/pkg/k8s/manifestival.go @@ -5,8 +5,8 @@ import ( "github.com/manifestival/manifestival" ) -func GetManifestivalClient() (manifestival.Client, error) { - config, err := GetClientConfig().ClientConfig() +func GetManifestivalClient(kc *Client) (manifestival.Client, error) { + config, err := kc.ClientConfig() if err != nil { return nil, err } diff --git a/pkg/k8s/openshift.go b/pkg/k8s/openshift.go index b537ca47fc..6d6a35fd3b 100644 --- a/pkg/k8s/openshift.go +++ b/pkg/k8s/openshift.go @@ -6,7 +6,6 @@ import ( "encoding/pem" "errors" "strings" - "sync" "time" v1 "k8s.io/api/core/v1" @@ -24,8 +23,23 @@ const ( openShiftRegistryHostPort = openShiftRegistryHost + ":5000" ) -func GetOpenShiftServiceCA(ctx context.Context) (*x509.Certificate, error) { - client, ns, err := NewClientAndResolvedNamespace("") +// IsOpenshift detects whether the target cluster is OpenShift by checking +// for the route.openshift.io API group. The result is cached per Client instance. +func (c *Client) IsOpenshift() bool { + c.openShiftOnce.Do(func() { + clientset, err := c.Clientset() + if err != nil { + return + } + _, err = clientset.Discovery().ServerResourcesForGroupVersion("route.openshift.io/v1") + // if this group version is found == Openshift cluster + c.openshift = err == nil + }) + return c.openshift +} + +func (c *Client) GetOpenShiftServiceCA(ctx context.Context) (*x509.Certificate, error) { + client, ns, err := c.ClientAndNamespace("") if err != nil { return nil, err } @@ -85,12 +99,11 @@ func GetOpenShiftServiceCA(ctx context.Context) (*x509.Certificate, error) { } } -func GetDefaultOpenShiftRegistry() string { - ns, _ := GetDefaultNamespace() +func GetDefaultOpenShiftRegistry(c *Client) string { + ns, _ := c.DefaultNamespace() if ns == "" { ns = "default" } - return openShiftRegistryHostPort + "/" + ns } @@ -100,10 +113,8 @@ func IsOpenShiftInternalRegistry(registry string) bool { return strings.HasPrefix(registry, openShiftRegistryHost) } -func GetOpenShiftDockerCredentialLoaders() []creds.CredentialsCallback { - conf := GetClientConfig() - - rawConf, err := conf.RawConfig() +func GetOpenShiftDockerCredentialLoaders(c *Client) []creds.CredentialsCallback { + rawConf, err := c.RawConfig() if err != nil { return nil } @@ -130,42 +141,6 @@ func GetOpenShiftDockerCredentialLoaders() []creds.CredentialsCallback { } -var isOpenShift bool -var checkOpenShiftOnce sync.Once - -// SetOpenShiftForTest overrides OpenShift detection for testing. -// Returns a cleanup function that restores the previous state. -func SetOpenShiftForTest(val bool) func() { - checkOpenShiftOnce.Do(func() {}) // ensure real detection won't run - prev := isOpenShift - isOpenShift = val - return func() { isOpenShift = prev } -} - -func IsOpenShift() bool { - checkOpenShiftOnce.Do(func() { - isOpenShift = false - client, err := NewKubernetesClientset() - if err != nil { - return - } - - // Detect OpenShift by checking for OpenShift-specific API groups - // This is reliable and works even with restrictive RBAC, unlike checking - // for namespaces/services which can produce false positives when forbidden - discoveryClient := client.Discovery() - - // Check for route.openshift.io API group (Routes are OpenShift-specific) - _, err = discoveryClient.ServerResourcesForGroupVersion("route.openshift.io/v1") - if err == nil { - // API group exists - this is OpenShift - isOpenShift = true - } - // If NotFound or any other error, this is most likely not OpenShift - }) - return isOpenShift -} - const ( annotationOpenShiftVcsUri = "app.openshift.io/vcs-uri" annotationOpenShiftVcsRef = "app.openshift.io/vcs-ref" diff --git a/pkg/k8s/persistent_volumes.go b/pkg/k8s/persistent_volumes.go index 658a4a90de..e4d7353338 100644 --- a/pkg/k8s/persistent_volumes.go +++ b/pkg/k8s/persistent_volumes.go @@ -23,8 +23,8 @@ import ( k8sclientcmd "k8s.io/client-go/tools/clientcmd" ) -func GetPersistentVolumeClaim(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func GetPersistentVolumeClaim(ctx context.Context, kc *Client, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return nil, err } @@ -32,8 +32,8 @@ func GetPersistentVolumeClaim(ctx context.Context, name, namespaceOverride strin return client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, name, metav1.GetOptions{}) } -func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClassName string) (err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func CreatePersistentVolumeClaim(ctx context.Context, kc *Client, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClassName string) (err error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return } @@ -63,8 +63,8 @@ func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride st return } -func DeletePersistentVolumeClaims(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func DeletePersistentVolumeClaims(ctx context.Context, kc *Client, namespaceOverride string, listOptions metav1.ListOptions) (err error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return } @@ -73,8 +73,8 @@ func DeletePersistentVolumeClaims(ctx context.Context, namespaceOverride string, } // DeletePersistentVolumeClaim deletes a single PVC by name -func DeletePersistentVolumeClaim(ctx context.Context, name, namespaceOverride string) error { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func DeletePersistentVolumeClaim(ctx context.Context, kc *Client, name, namespaceOverride string) error { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return err } @@ -87,8 +87,8 @@ func DeletePersistentVolumeClaim(ctx context.Context, name, namespaceOverride st } // WaitForPVCDeletion waits for a PVC to be fully deleted (not just in Terminating state) -func WaitForPVCDeletion(ctx context.Context, name, namespaceOverride string) error { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func WaitForPVCDeletion(ctx context.Context, kc *Client, name, namespaceOverride string) error { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return err } @@ -116,17 +116,16 @@ func WaitForPVCDeletion(ctx context.Context, name, namespaceOverride string) err var TarImage = "ghcr.io/knative/func-utils:v2" // UploadToVolume uploads files (passed in form of tar stream) into volume. -func UploadToVolume(ctx context.Context, content io.Reader, claimName, namespace string) error { - return runWithVolumeMounted(ctx, TarImage, []string{"sh", "-c", "umask 0000 && exec tar -xmf -"}, content, claimName, namespace) +func UploadToVolume(ctx context.Context, kc *Client, content io.Reader, claimName, namespace string) error { + return runWithVolumeMounted(ctx, kc, TarImage, []string{"sh", "-c", "umask 0000 && exec tar -xmf -"}, content, claimName, namespace) } // Runs a pod with given image, command and stdin // while having the volume mounted and working directory set to it. -func runWithVolumeMounted(ctx context.Context, podImage string, podCommand []string, podInput io.Reader, claimName, namespace string) error { +func runWithVolumeMounted(ctx context.Context, kc *Client, podImage string, podCommand []string, podInput io.Reader, claimName, namespace string) error { var err error - cliConf := GetClientConfig() - restConf, err := cliConf.ClientConfig() + restConf, err := kc.ClientConfig() if err != nil { return fmt.Errorf("cannot get client config: %w", err) } @@ -143,7 +142,7 @@ func runWithVolumeMounted(ctx context.Context, podImage string, podCommand []str } if namespace == "" { - namespace, err = GetDefaultNamespace() + namespace, err = kc.DefaultNamespace() if err != nil { return fmt.Errorf("cannot get namespace: %w", err) } @@ -166,7 +165,7 @@ func runWithVolumeMounted(ctx context.Context, podImage string, podCommand []str Annotations: nil, }, Spec: corev1.PodSpec{ - SecurityContext: defaultPodSecurityContext(), + SecurityContext: defaultPodSecurityContext(kc), Containers: []corev1.Container{ { Name: podName, @@ -200,7 +199,7 @@ func runWithVolumeMounted(ctx context.Context, podImage string, podCommand []str localCtx, cancel := context.WithCancel(ctx) defer cancel() - ready := podReady(localCtx, client.CoreV1(), podName, namespace) + ready := podReady(localCtx, kc, client.CoreV1(), podName, namespace) _, err = pods.Create(ctx, pod, metav1.CreateOptions{}) if err != nil { @@ -281,8 +280,8 @@ func (t *tsBuff) Write(p []byte) (n int, err error) { // ListPersistentVolumeClaimsNamesIfConnected lists names of PersistentVolumeClaims present and the current k8s context // returns empty list, if not connected to any cluster -func ListPersistentVolumeClaimsNamesIfConnected(ctx context.Context, namespaceOverride string) (names []string, err error) { - names, err = listPersistentVolumeClaimsNames(ctx, namespaceOverride) +func ListPersistentVolumeClaimsNamesIfConnected(ctx context.Context, kc *Client, namespaceOverride string) (names []string, err error) { + names, err = listPersistentVolumeClaimsNames(ctx, kc, namespaceOverride) if err != nil { // not logged our authorized to access resources if k8serrors.IsForbidden(err) || k8serrors.IsUnauthorized(err) || k8serrors.IsInvalid(err) || k8serrors.IsTimeout(err) { @@ -311,8 +310,8 @@ func ListPersistentVolumeClaimsNamesIfConnected(ctx context.Context, namespaceOv return } -func listPersistentVolumeClaimsNames(ctx context.Context, namespaceOverride string) (names []string, err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func listPersistentVolumeClaimsNames(ctx context.Context, kc *Client, namespaceOverride string) (names []string, err error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return } diff --git a/pkg/k8s/persistent_volumes_int_test.go b/pkg/k8s/persistent_volumes_int_test.go index 2897a4946d..7d38d350b7 100644 --- a/pkg/k8s/persistent_volumes_int_test.go +++ b/pkg/k8s/persistent_volumes_int_test.go @@ -16,6 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/rand" + "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) @@ -24,7 +25,8 @@ func TestInt_UploadToVolume(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), time.Minute*5) t.Cleanup(cancel) - cliSet, testingNS, err := k8s.NewClientAndResolvedNamespace("") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", functions.Local{})) + cliSet, testingNS, err := kc.ClientAndNamespace("") if err != nil { t.Fatal(err) } @@ -32,7 +34,7 @@ func TestInt_UploadToVolume(t *testing.T) { rnd := rand.String(5) testingPVCName := "testing-pvc-" + rnd - err = k8s.CreatePersistentVolumeClaim(ctx, testingPVCName, testingNS, + err = k8s.CreatePersistentVolumeClaim(ctx, kc, testingPVCName, testingNS, nil, nil, corev1.ReadWriteOnce, *resource.NewQuantity(1024, resource.DecimalSI), "") if err != nil { @@ -48,7 +50,7 @@ func TestInt_UploadToVolume(t *testing.T) { t.Log("created PVC: " + testingPVCName) // First, test error handling by uploading empty content stream. - err = k8s.UploadToVolume(ctx, &bytes.Buffer{}, testingPVCName, testingNS) + err = k8s.UploadToVolume(ctx, kc, &bytes.Buffer{}, testingPVCName, testingNS) if err == nil || !strings.Contains(err.Error(), "does not look like a tar") { t.Error("got error, or error with unexpected message") } @@ -59,7 +61,7 @@ func TestInt_UploadToVolume(t *testing.T) { } t.Cleanup(func() { f.Close() }) - err = k8s.UploadToVolume(ctx, f, testingPVCName, testingNS) + err = k8s.UploadToVolume(ctx, kc, f, testingPVCName, testingNS) if err != nil { t.Fatal(err) } @@ -126,7 +128,7 @@ func TestInt_UploadToVolume(t *testing.T) { } t.Log("the testing pod has exited") - out, err := k8s.GetPodLogs(ctx, testingNS, testingPodName, testingPodName) + out, err := k8s.GetPodLogs(ctx, kc, testingNS, testingPodName, testingPodName) if err != nil { t.Fatal(err) } @@ -138,7 +140,8 @@ func TestInt_UploadToVolume(t *testing.T) { func TestInt_ListPersistentVolumeClaimsNamesIfConnectedWrongKubeconfig(t *testing.T) { t.Setenv("KUBECONFIG", "/tmp/non-existent.config") - _, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(t.Context(), "") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", functions.Local{})) + _, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(t.Context(), kc, "") if err != nil { t.Fatal(err) } @@ -146,7 +149,8 @@ func TestInt_ListPersistentVolumeClaimsNamesIfConnectedWrongKubeconfig(t *testin func TestInt_ListPersistentVolumeClaimsNamesIfConnectedWrongKubernentesMaster(t *testing.T) { t.Setenv("KUBERNETES_MASTER", "/tmp/non-existent.config") - _, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(t.Context(), "") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", functions.Local{})) + _, err := k8s.ListPersistentVolumeClaimsNamesIfConnected(t.Context(), kc, "") if err != nil { t.Fatal(err) } diff --git a/pkg/k8s/remover.go b/pkg/k8s/remover.go index 563ef575c2..9eddb3b37d 100644 --- a/pkg/k8s/remover.go +++ b/pkg/k8s/remover.go @@ -10,14 +10,16 @@ import ( fn "knative.dev/func/pkg/functions" ) -func NewRemover(verbose bool) *Remover { +func NewRemover(kc *Client, verbose bool) *Remover { return &Remover{ + kc: kc, verbose: verbose, } } type Remover struct { verbose bool + kc *Client } func (remover *Remover) Remove(ctx context.Context, name, ns string) error { @@ -26,7 +28,7 @@ func (remover *Remover) Remove(ctx context.Context, name, ns string) error { return fn.ErrNamespaceRequired } - clientset, err := NewKubernetesClientset() + clientset, err := remover.kc.Clientset() if err != nil { return fmt.Errorf("could not setup kubernetes clientset: %w", err) } diff --git a/pkg/k8s/remover_int_test.go b/pkg/k8s/remover_int_test.go index 28c86237b4..13977398e6 100644 --- a/pkg/k8s/remover_int_test.go +++ b/pkg/k8s/remover_int_test.go @@ -10,10 +10,11 @@ import ( ) func TestInt_Remove(t *testing.T) { + kc := defaultKc() removertesting.TestInt_Remove(t, - k8s.NewRemover(true), - k8s.NewDeployer(k8s.WithDeployerVerbose(true)), - k8s.NewDescriber(true), - k8s.NewLister(true), + k8s.NewRemover(kc, true), + k8s.NewDeployer(kc, k8s.WithDeployerVerbose(true)), + k8s.NewDescriber(kc, true), + k8s.NewLister(kc, true), k8s.KubernetesDeployerName) } diff --git a/pkg/k8s/secrets.go b/pkg/k8s/secrets.go index f8380a3414..977dd6a8ca 100644 --- a/pkg/k8s/secrets.go +++ b/pkg/k8s/secrets.go @@ -15,8 +15,8 @@ import ( k8sclientcmd "k8s.io/client-go/tools/clientcmd" ) -func GetSecret(ctx context.Context, name, namespaceOverride string) (*corev1.Secret, error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func GetSecret(ctx context.Context, kc *Client, name, namespaceOverride string) (*corev1.Secret, error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return nil, err } @@ -26,8 +26,8 @@ func GetSecret(ctx context.Context, name, namespaceOverride string) (*corev1.Sec // ListSecretsNamesIfConnected lists names of Secrets present and the current k8s context // returns empty list, if not connected to any cluster -func ListSecretsNamesIfConnected(ctx context.Context, namespaceOverride string) (names []string, err error) { - names, err = listSecretsNames(ctx, namespaceOverride) +func ListSecretsNamesIfConnected(ctx context.Context, kc *Client, namespaceOverride string) (names []string, err error) { + names, err = listSecretsNames(ctx, kc, namespaceOverride) if err != nil { // not logged our authorized to access resources if k8serrors.IsForbidden(err) || k8serrors.IsUnauthorized(err) || k8serrors.IsInvalid(err) || k8serrors.IsTimeout(err) { @@ -56,8 +56,8 @@ func ListSecretsNamesIfConnected(ctx context.Context, namespaceOverride string) return } -func listSecretsNames(ctx context.Context, namespaceOverride string) (names []string, err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func listSecretsNames(ctx context.Context, kc *Client, namespaceOverride string) (names []string, err error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return } @@ -74,8 +74,8 @@ func listSecretsNames(ctx context.Context, namespaceOverride string) (names []st return } -func DeleteSecrets(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) (err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func DeleteSecrets(ctx context.Context, kc *Client, namespaceOverride string, listOptions metav1.ListOptions) (err error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return } @@ -83,7 +83,7 @@ func DeleteSecrets(ctx context.Context, namespaceOverride string, listOptions me return client.CoreV1().Secrets(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions) } -func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, username, password, server string) (err error) { +func EnsureDockerRegistrySecretExist(ctx context.Context, kc *Client, name, namespace string, labels, annotations map[string]string, username, password, server string) (err error) { dockerConfigJSONContent, err := HandleDockerCfgJSONContent(username, password, "", server) if err != nil { return @@ -101,18 +101,18 @@ func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverrid } secret.Data["config.json"] = dockerConfigJSONContent - return EnsureSecretExist(ctx, secret, namespaceOverride) + return EnsureSecretExist(ctx, kc, secret, namespace) } -func EnsureSecretExist(ctx context.Context, secret corev1.Secret, namespaceOverride string) (err error) { - client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) +func EnsureSecretExist(ctx context.Context, kc *Client, secret corev1.Secret, namespaceOverride string) (err error) { + client, namespace, err := kc.ClientAndNamespace(namespaceOverride) if err != nil { return } // Check whether Secret with specified name exist secretNotFound := false - existingSecret, err := GetSecret(ctx, secret.Name, namespace) + existingSecret, err := GetSecret(ctx, kc, secret.Name, namespace) if err != nil { if !k8serrors.IsNotFound(err) { return diff --git a/pkg/k8s/secrets_test.go b/pkg/k8s/secrets_test.go index eafcc795f1..058659759c 100644 --- a/pkg/k8s/secrets_test.go +++ b/pkg/k8s/secrets_test.go @@ -3,12 +3,14 @@ package k8s_test import ( "testing" + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) func TestListSecretsNamesIfConnectedWrongKubeconfig(t *testing.T) { t.Setenv("KUBECONFIG", "/tmp/non-existent.config") - _, err := k8s.ListSecretsNamesIfConnected(t.Context(), "") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + _, err := k8s.ListSecretsNamesIfConnected(t.Context(), kc, "") if err != nil { t.Fatal(err) } @@ -16,7 +18,8 @@ func TestListSecretsNamesIfConnectedWrongKubeconfig(t *testing.T) { func TestListSecretsNamesIfConnectedWrongKubernentesMaster(t *testing.T) { t.Setenv("KUBERNETES_MASTER", "/tmp/non-existent.config") - _, err := k8s.ListSecretsNamesIfConnected(t.Context(), "") + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + _, err := k8s.ListSecretsNamesIfConnected(t.Context(), kc, "") if err != nil { t.Fatal(err) } diff --git a/pkg/k8s/security_context.go b/pkg/k8s/security_context.go index 4ddaecbf01..8c2b9af89f 100644 --- a/pkg/k8s/security_context.go +++ b/pkg/k8s/security_context.go @@ -16,11 +16,11 @@ import ( // with Tekton buildpack tasks that mount volumes with group ownership 0. // This does not violate the restricted profile (which checks UID, not GID) but is // tracked for remediation in https://github.com/knative/func/issues/3517. -func defaultPodSecurityContext() *corev1.PodSecurityContext { +func defaultPodSecurityContext(kc *Client) *corev1.PodSecurityContext { runAsNonRoot := true seccompProfile := &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} - if IsOpenShift() { + if kc.IsOpenshift() { // On OpenShift, SCCs manage RunAsUser/RunAsGroup/FSGroup; setting them // here would conflict with the namespace's SCC UID range policy. // Only set the fields required by the restricted PSA profile. diff --git a/pkg/k8s/security_context_test.go b/pkg/k8s/security_context_test.go index 246171cd09..26b1b37a28 100644 --- a/pkg/k8s/security_context_test.go +++ b/pkg/k8s/security_context_test.go @@ -6,15 +6,14 @@ import ( corev1 "k8s.io/api/core/v1" ) -// Note: SetOpenShiftForTest mutates a package-level bool without a mutex. -// These tests must not be run with t.Parallel() until that is addressed. -// See openshift.go:SetOpenShiftForTest. +func testClient(openshift bool) *Client { + return NewClientWithOpenShift(nil, openshift) +} func TestDefaultPodSecurityContext_NonOpenShift(t *testing.T) { - cleanup := SetOpenShiftForTest(false) - defer cleanup() + kc := testClient(false) - sc := defaultPodSecurityContext() + sc := defaultPodSecurityContext(kc) if sc == nil { t.Fatal("expected non-nil PodSecurityContext on non-OpenShift") } @@ -36,10 +35,9 @@ func TestDefaultPodSecurityContext_NonOpenShift(t *testing.T) { } func TestDefaultPodSecurityContext_OpenShift(t *testing.T) { - cleanup := SetOpenShiftForTest(true) - defer cleanup() + kc := testClient(true) - sc := defaultPodSecurityContext() + sc := defaultPodSecurityContext(kc) if sc == nil { t.Fatal("expected non-nil PodSecurityContext on OpenShift") } @@ -52,7 +50,6 @@ func TestDefaultPodSecurityContext_OpenShift(t *testing.T) { if sc.SeccompProfile.Type != corev1.SeccompProfileTypeRuntimeDefault { t.Errorf("expected SeccompProfile.Type=RuntimeDefault, got %v", sc.SeccompProfile.Type) } - // On OpenShift SCCs manage UID/GID; these must not be set. if sc.RunAsUser != nil { t.Errorf("expected RunAsUser to be nil on OpenShift, got %d", *sc.RunAsUser) } @@ -92,13 +89,6 @@ func TestDefaultSecurityContext(t *testing.T) { } } -// TestRestrictedProfileCompliance verifies both security context helpers together -// satisfy all four fields required by the Kubernetes "restricted" pod security -// profile on both OpenShift and vanilla Kubernetes clusters. -// -// Note: this validates Go struct fields only. End-to-end admission validation -// against a real restricted namespace is covered by the integration test suite -// (make test-full). func TestRestrictedProfileCompliance(t *testing.T) { for _, openshift := range []bool{false, true} { openshift := openshift @@ -107,17 +97,14 @@ func TestRestrictedProfileCompliance(t *testing.T) { name = "openshift" } t.Run(name, func(t *testing.T) { - cleanup := SetOpenShiftForTest(openshift) - defer cleanup() + kc := testClient(openshift) - pod := defaultPodSecurityContext() + pod := defaultPodSecurityContext(kc) ctr := defaultSecurityContext() - // restricted requires: allowPrivilegeEscalation=false (container level) if ctr.AllowPrivilegeEscalation == nil || *ctr.AllowPrivilegeEscalation { t.Error("restricted violation: AllowPrivilegeEscalation must be false") } - // restricted requires: capabilities.drop must include ALL (container level) hasDropAll := false if ctr.Capabilities != nil { for _, cap := range ctr.Capabilities.Drop { @@ -130,17 +117,14 @@ func TestRestrictedProfileCompliance(t *testing.T) { if !hasDropAll { t.Error("restricted violation: capabilities.drop must include ALL") } - // restricted requires: runAsNonRoot=true (pod or container level) - podHasRunAsNonRoot := pod != nil && pod.RunAsNonRoot != nil && *pod.RunAsNonRoot - ctrHasRunAsNonRoot := ctr.RunAsNonRoot != nil && *ctr.RunAsNonRoot - if !podHasRunAsNonRoot && !ctrHasRunAsNonRoot { - t.Error("restricted violation: runAsNonRoot must be true at pod or container level") + if pod.RunAsNonRoot == nil || !*pod.RunAsNonRoot { + t.Error("restricted violation: runAsNonRoot must be true") + } + if pod.SeccompProfile == nil { + t.Error("restricted violation: seccompProfile must be set at pod level") } - // restricted requires: seccompProfile (pod or container level) - podHasSeccomp := pod != nil && pod.SeccompProfile != nil - ctrHasSeccomp := ctr.SeccompProfile != nil - if !podHasSeccomp && !ctrHasSeccomp { - t.Error("restricted violation: seccompProfile must be set at pod or container level") + if ctr.SeccompProfile == nil { + t.Error("restricted violation: seccompProfile must be set at container level") } }) } diff --git a/pkg/k8s/serviceaccount.go b/pkg/k8s/serviceaccount.go index 8180cae270..3ef4439ee6 100644 --- a/pkg/k8s/serviceaccount.go +++ b/pkg/k8s/serviceaccount.go @@ -6,8 +6,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func GetServiceAccount(ctx context.Context, referencedServiceAccount, namespace string) error { - k8sClient, err := NewKubernetesClientset() +func GetServiceAccount(ctx context.Context, kc *Client, referencedServiceAccount, namespace string) error { + k8sClient, err := kc.Clientset() if err != nil { return err } diff --git a/pkg/keda/client.go b/pkg/keda/client.go index b39531281f..bbaa274e56 100644 --- a/pkg/keda/client.go +++ b/pkg/keda/client.go @@ -7,8 +7,8 @@ import ( "knative.dev/func/pkg/k8s" ) -func NewHTTPScaledObjectClientset() (*httpv1alpha1.Clientset, error) { - restConfig, err := k8s.GetClientConfig().ClientConfig() +func NewHTTPScaledObjectClientset(kc *k8s.Client) (*httpv1alpha1.Clientset, error) { + restConfig, err := kc.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to get clientconfig: %w", err) } diff --git a/pkg/keda/deployer.go b/pkg/keda/deployer.go index 17300bfda1..26972d7e00 100644 --- a/pkg/keda/deployer.go +++ b/pkg/keda/deployer.go @@ -29,11 +29,14 @@ type Deployer struct { verbose bool decorator deployer.DeployDecorator + kc *k8s.Client } -func NewDeployer(opts ...DeployerOpt) *Deployer { +func NewDeployer(kc *k8s.Client, opts ...DeployerOpt) *Deployer { d := &Deployer{ + kc: kc, Deployer: *k8s.NewDeployer( + kc, // init with the kedaDeployerDecorator to have the correct deployer labels&annotations k8s.WithDeployerDecorator(&kedaDeployerDecorator{}), ), @@ -100,7 +103,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu // create additional required keda resources namespace := deployResult.Namespace - k8sClientset, err := k8s.NewKubernetesClientset() + k8sClientset, err := d.kc.Clientset() if err != nil { return fn.DeploymentResult{}, fmt.Errorf("failed to create K8sClientset: %v", err) } @@ -265,7 +268,7 @@ func (d *Deployer) ensureHTTPScaledObject(ctx context.Context, f fn.Function, na return fmt.Errorf("failed to generate http scaled object: %w", err) } - httpScaledObjectClientset, err := NewHTTPScaledObjectClientset() + httpScaledObjectClientset, err := NewHTTPScaledObjectClientset(d.kc) if err != nil { return fmt.Errorf("failed to create HTTPScaledObject clientset: %v", err) } diff --git a/pkg/keda/deployer_int_test.go b/pkg/keda/deployer_int_test.go index 6e22d09502..78f23edd41 100644 --- a/pkg/keda/deployer_int_test.go +++ b/pkg/keda/deployer_int_test.go @@ -6,72 +6,86 @@ import ( "testing" deployertesting "knative.dev/func/pkg/deployer/testing" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/keda" ) +func defaultKc() *k8s.Client { + return k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) +} + func TestInt_FullPath(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_FullPath(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewLister(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewLister(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_Deploy(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Deploy(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_Metadata(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Metadata(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_Events(t *testing.T) { t.Skip("Keda deployer does not support func subscribe yet") + kc := defaultKc() deployertesting.TestInt_Events(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_Scale(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Scale(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_EnvsUpdate(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_EnvsUpdate(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_ResourceValidationOnFirstDeploy(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_ResourceValidationOnFirstDeploy(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } func TestInt_OperatorSync(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_OperatorSync(t, - keda.NewDeployer(keda.WithDeployerVerbose(false)), - keda.NewRemover(false), - keda.NewDescriber(false), + keda.NewDeployer(kc, keda.WithDeployerVerbose(false)), + keda.NewRemover(kc, false), + keda.NewDescriber(kc, false), keda.KedaDeployerName) } diff --git a/pkg/keda/describer.go b/pkg/keda/describer.go index 946f6e2a44..613e3fea2b 100644 --- a/pkg/keda/describer.go +++ b/pkg/keda/describer.go @@ -18,6 +18,7 @@ import ( type Describer struct { verbose bool transport http.RoundTripper + kc *k8s.Client } type DescriberOpt func(*Describer) @@ -28,8 +29,8 @@ func WithDescriberTransport(transport http.RoundTripper) DescriberOpt { } } -func NewDescriber(verbose bool, opts ...DescriberOpt) *Describer { - d := &Describer{verbose: verbose} +func NewDescriber(kc *k8s.Client, verbose bool, opts ...DescriberOpt) *Describer { + d := &Describer{kc: kc, verbose: verbose} for _, o := range opts { o(d) } @@ -42,7 +43,7 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In return fn.Instance{}, fmt.Errorf("function namespace is required when describing %q", name) } - clientset, err := k8s.NewKubernetesClientset() + clientset, err := d.kc.Clientset() if err != nil { return fn.Instance{}, fmt.Errorf("unable to create k8s client: %v", err) } @@ -66,7 +67,7 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In // We're responsible, for this function --> proceed... - httpScaledObjectClientset, err := NewHTTPScaledObjectClientset() + httpScaledObjectClientset, err := NewHTTPScaledObjectClientset(d.kc) if err != nil { return fn.Instance{}, fmt.Errorf("unable to create HTTPScaledObject client: %v", err) } diff --git a/pkg/keda/describer_int_test.go b/pkg/keda/describer_int_test.go index bc32ccdd70..c87bfbbe97 100644 --- a/pkg/keda/describer_int_test.go +++ b/pkg/keda/describer_int_test.go @@ -10,9 +10,10 @@ import ( ) func TestInt_Describe(t *testing.T) { + kc := defaultKc() describertesting.TestInt_Describe(t, - keda.NewDescriber(true), - keda.NewDeployer(keda.WithDeployerVerbose(true)), - keda.NewRemover(true), + keda.NewDescriber(kc, true), + keda.NewDeployer(kc, keda.WithDeployerVerbose(true)), + keda.NewRemover(kc, true), keda.KedaDeployerName) } diff --git a/pkg/keda/lister.go b/pkg/keda/lister.go index 4d100430ae..4569ee0a0e 100644 --- a/pkg/keda/lister.go +++ b/pkg/keda/lister.go @@ -15,21 +15,23 @@ import ( type Lister struct { verbose bool + kc *k8s.Client } -func NewLister(verbose bool) fn.Lister { +func NewLister(kc *k8s.Client, verbose bool) fn.Lister { return &Lister{ + kc: kc, verbose: verbose, } } func (l *Lister) List(ctx context.Context, namespace string) ([]fn.ListItem, error) { - clientset, err := k8s.NewKubernetesClientset() + clientset, err := l.kc.Clientset() if err != nil { return nil, fmt.Errorf("unable to create k8s client: %v", err) } - httpScaledObjectClientset, err := NewHTTPScaledObjectClientset() + httpScaledObjectClientset, err := NewHTTPScaledObjectClientset(l.kc) if err != nil { return nil, fmt.Errorf("unable to create HTTPScaledObject client: %v", err) } diff --git a/pkg/keda/lister_int_test.go b/pkg/keda/lister_int_test.go index 8160086893..c45fcfef04 100644 --- a/pkg/keda/lister_int_test.go +++ b/pkg/keda/lister_int_test.go @@ -10,10 +10,11 @@ import ( ) func TestInt_List(t *testing.T) { + kc := defaultKc() listertesting.TestInt_List(t, - keda.NewLister(true), - keda.NewDeployer(keda.WithDeployerVerbose(true)), - keda.NewDescriber(true), - keda.NewRemover(true), + keda.NewLister(kc, true), + keda.NewDeployer(kc, keda.WithDeployerVerbose(true)), + keda.NewDescriber(kc, true), + keda.NewRemover(kc, true), keda.KedaDeployerName) } diff --git a/pkg/keda/remover.go b/pkg/keda/remover.go index 097d020856..b3bc964290 100644 --- a/pkg/keda/remover.go +++ b/pkg/keda/remover.go @@ -11,14 +11,16 @@ import ( "knative.dev/func/pkg/k8s" ) -func NewRemover(verbose bool) *Remover { +func NewRemover(kc *k8s.Client, verbose bool) *Remover { return &Remover{ + kc: kc, verbose: verbose, } } type Remover struct { verbose bool + kc *k8s.Client } func (remover *Remover) Remove(ctx context.Context, name, ns string) error { @@ -27,7 +29,7 @@ func (remover *Remover) Remove(ctx context.Context, name, ns string) error { return fn.ErrNamespaceRequired } - clientset, err := k8s.NewKubernetesClientset() + clientset, err := remover.kc.Clientset() if err != nil { return fmt.Errorf("could not setup kubernetes clientset: %w", err) } diff --git a/pkg/keda/remover_int_test.go b/pkg/keda/remover_int_test.go index 4dfa6e630c..b764e0155e 100644 --- a/pkg/keda/remover_int_test.go +++ b/pkg/keda/remover_int_test.go @@ -10,10 +10,11 @@ import ( ) func TestInt_Remove(t *testing.T) { + kc := defaultKc() removertesting.TestInt_Remove(t, - keda.NewRemover(true), - keda.NewDeployer(keda.WithDeployerVerbose(true)), - keda.NewDescriber(true), - keda.NewLister(true), + keda.NewRemover(kc, true), + keda.NewDeployer(kc, keda.WithDeployerVerbose(true)), + keda.NewDescriber(kc, true), + keda.NewLister(kc, true), keda.KedaDeployerName) } diff --git a/pkg/knative/client.go b/pkg/knative/client.go index cfed4a5f8b..3eb404225a 100644 --- a/pkg/knative/client.go +++ b/pkg/knative/client.go @@ -14,12 +14,8 @@ import ( "knative.dev/func/pkg/k8s" ) -func NewServingClient(namespace string) (clientservingv1.KnServingClient, error) { - if err := validateKubeconfigFile(); err != nil { - return nil, err - } - - restConfig, err := k8s.GetClientConfig().ClientConfig() +func NewServingClient(k8sClient *k8s.Client, namespace string) (clientservingv1.KnServingClient, error) { + restConfig, err := k8sClient.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to create new serving client: %v", err) } @@ -34,12 +30,8 @@ func NewServingClient(namespace string) (clientservingv1.KnServingClient, error) return client, nil } -func NewEventingClient(namespace string) (clienteventingv1.KnEventingClient, error) { - if err := validateKubeconfigFile(); err != nil { - return nil, err - } - - restConfig, err := k8s.GetClientConfig().ClientConfig() +func NewEventingClient(k8sClient *k8s.Client, namespace string) (clienteventingv1.KnEventingClient, error) { + restConfig, err := k8sClient.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to create new serving client: %v", err) } diff --git a/pkg/knative/deployer.go b/pkg/knative/deployer.go index 1bd011d142..0f3a63dfac 100644 --- a/pkg/knative/deployer.go +++ b/pkg/knative/deployer.go @@ -51,11 +51,12 @@ type Deployer struct { // verbose logging enablement flag. verbose bool + k8sClient *k8s.Client decorator deployer.DeployDecorator } -func NewDeployer(opts ...DeployerOpt) *Deployer { - d := &Deployer{} +func NewDeployer(k8sClient *k8s.Client, opts ...DeployerOpt) *Deployer { + d := &Deployer{k8sClient: k8sClient} for _, opt := range opts { opt(d) @@ -83,7 +84,7 @@ func (d *Deployer) isImageInPrivateRegistry(ctx context.Context, client clientse if err != nil { return false } - k8sClient, err := k8s.NewKubernetesClientset() + k8sClient, err := d.k8sClient.Clientset() if err != nil { return false } @@ -106,7 +107,7 @@ func (d *Deployer) isImageInPrivateRegistry(ctx context.Context, client clientse return false } -func onClusterFix(f fn.Function) fn.Function { +func onClusterFix(f fn.Function, kc *k8s.Client) fn.Function { // This only exists because of a bootstrapping problem with On-Cluster // builds: It appears that, when sending a function to be built on-cluster // the target namespace is not being transmitted in the pipeline @@ -115,13 +116,13 @@ func onClusterFix(f fn.Function) fn.Function { // earlier versions of this logic relied entirely on the current // kubernetes context. if f.Namespace == "" && f.Deploy.Namespace == "" { - f.Namespace, _ = k8s.GetDefaultNamespace() + f.Namespace, _ = kc.DefaultNamespace() } return f } func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResult, error) { - f = onClusterFix(f) + f = onClusterFix(f, d.k8sClient) // Choosing f.Namespace vs f.Deploy.Namespace: // This is minimal logic currently required of all deployer impls. // If f.Namespace is defined, this is the (possibly new) target @@ -150,17 +151,17 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu } // Clients - client, err := NewServingClient(namespace) + client, err := NewServingClient(d.k8sClient, namespace) if err != nil { return fn.DeploymentResult{}, wrapDeployerClientError(err) } - eventingClient, err := NewEventingClient(namespace) + eventingClient, err := NewEventingClient(d.k8sClient, namespace) if err != nil { return fn.DeploymentResult{}, wrapDeployerClientError(err) } // check if 'dapr-system' namespace exists daprInstalled := false - k8sClient, err := k8s.NewKubernetesClientset() + k8sClient, err := d.k8sClient.Clientset() if err != nil { return fn.DeploymentResult{}, wrapDeployerClientError(err) } @@ -169,7 +170,7 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu daprInstalled = true } - t := fnhttp.NewRoundTripper(fnhttp.WithOpenShiftServiceCA(), fnhttp.WithInsecureSkipVerify(f.RegistryInsecure)) + t := fnhttp.NewRoundTripper(d.k8sClient, fnhttp.WithOpenShiftServiceCA(d.k8sClient), fnhttp.WithInsecureSkipVerify(f.RegistryInsecure)) defer func(t fnhttp.RoundTripCloser) { _ = t.Close() }(t) @@ -197,7 +198,7 @@ consider using the --image-pull-secret flag, or setting up pull secrets manually } since := time.Now() go func() { - _ = GetKServiceLogs(ctx, namespace, f.Name, f.Deploy.Image, &since, out) + _ = GetKServiceLogs(ctx, d.k8sClient, namespace, f.Name, f.Deploy.Image, &since, out) }() previousService, err := client.GetService(ctx, f.Name) @@ -217,7 +218,7 @@ consider using the --image-pull-secret flag, or setting up pull secrets manually return fn.DeploymentResult{}, err } - err = k8s.CheckResourcesArePresent(ctx, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret) + err = k8s.CheckResourcesArePresent(ctx, d.k8sClient, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret) if err != nil { err = fmt.Errorf("knative deployer failed to generate the Knative Service: %v", err) return fn.DeploymentResult{}, err @@ -319,7 +320,7 @@ consider using the --image-pull-secret flag, or setting up pull secrets manually return fn.DeploymentResult{}, err } - err = k8s.CheckResourcesArePresent(ctx, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret) + err = k8s.CheckResourcesArePresent(ctx, d.k8sClient, namespace, &referencedSecrets, &referencedConfigMaps, &referencedPVCs, f.Deploy.ServiceAccountName, f.Deploy.ImagePullSecret) if err != nil { err = fmt.Errorf("knative deployer failed to update the Knative Service: %v", err) return fn.DeploymentResult{}, err diff --git a/pkg/knative/deployer_int_test.go b/pkg/knative/deployer_int_test.go index 5a0745a626..184afdc27e 100644 --- a/pkg/knative/deployer_int_test.go +++ b/pkg/knative/deployer_int_test.go @@ -6,70 +6,84 @@ import ( "testing" deployertesting "knative.dev/func/pkg/deployer/testing" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" ) +func defaultKc() *k8s.Client { + return k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) +} + func TestInt_FullPath(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_FullPath(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(true), - knative.NewLister(true), - knative.NewDescriber(true), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, true), + knative.NewLister(kc, true), + knative.NewDescriber(kc, true), knative.KnativeDeployerName) } func TestInt_Deploy(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Deploy(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } func TestInt_Metadata(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Metadata(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } func TestInt_Events(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Events(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } func TestInt_Scale(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_Scale(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } func TestInt_EnvsUpdate(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_EnvsUpdate(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } func TestInt_ResourceValidationOnFirstDeploy(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_ResourceValidationOnFirstDeploy(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } func TestInt_OperatorSync(t *testing.T) { + kc := defaultKc() deployertesting.TestInt_OperatorSync(t, - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(false), - knative.NewDescriber(false), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, false), + knative.NewDescriber(kc, false), knative.KnativeDeployerName) } diff --git a/pkg/knative/describer.go b/pkg/knative/describer.go index 37de836cd8..c3e251bbf4 100644 --- a/pkg/knative/describer.go +++ b/pkg/knative/describer.go @@ -18,6 +18,7 @@ import ( type Describer struct { verbose bool + k8sClient *k8s.Client transport http.RoundTripper } @@ -29,8 +30,8 @@ func WithDescriberTransport(transport http.RoundTripper) DescriberOpt { } } -func NewDescriber(verbose bool, opts ...DescriberOpt) *Describer { - d := &Describer{verbose: verbose} +func NewDescriber(k8sClient *k8s.Client, verbose bool, opts ...DescriberOpt) *Describer { + d := &Describer{verbose: verbose, k8sClient: k8sClient} for _, o := range opts { o(d) } @@ -47,12 +48,12 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In return fn.Instance{}, fmt.Errorf("function namespace is required when describing %q", name) } - servingClient, err := NewServingClient(namespace) + servingClient, err := NewServingClient(d.k8sClient, namespace) if err != nil { return fn.Instance{}, err } - eventingClient, err := NewEventingClient(namespace) + eventingClient, err := NewEventingClient(d.k8sClient, namespace) if err != nil { return fn.Instance{}, err } @@ -116,7 +117,7 @@ func (d *Describer) Describe(ctx context.Context, name, namespace string) (fn.In } // get used image (including the sha) - clientset, err := k8s.NewKubernetesClientset() + clientset, err := d.k8sClient.Clientset() if err != nil { return fn.Instance{}, fmt.Errorf("unable to create k8s client: %v", err) } diff --git a/pkg/knative/describer_int_test.go b/pkg/knative/describer_int_test.go index 4ddc036488..0c4577074e 100644 --- a/pkg/knative/describer_int_test.go +++ b/pkg/knative/describer_int_test.go @@ -10,9 +10,10 @@ import ( ) func TestInt_Describe(t *testing.T) { + kc := defaultKc() describertesting.TestInt_Describe(t, - knative.NewDescriber(true), - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewRemover(true), + knative.NewDescriber(kc, true), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewRemover(kc, true), knative.KnativeDeployerName) } diff --git a/pkg/knative/lister.go b/pkg/knative/lister.go index ddf86d4534..16e8a9a462 100644 --- a/pkg/knative/lister.go +++ b/pkg/knative/lister.go @@ -7,19 +7,21 @@ import ( "knative.dev/pkg/apis" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" ) type Lister struct { - verbose bool + verbose bool + k8sClient *k8s.Client } -func NewLister(verbose bool) *Lister { - return &Lister{verbose: verbose} +func NewLister(k8sClient *k8s.Client, verbose bool) *Lister { + return &Lister{verbose: verbose, k8sClient: k8sClient} } // List functions, optionally specifying a namespace. func (l *Lister) List(ctx context.Context, namespace string) ([]fn.ListItem, error) { - client, err := NewServingClient(namespace) + client, err := NewServingClient(l.k8sClient, namespace) if err != nil { return nil, err } diff --git a/pkg/knative/lister_int_test.go b/pkg/knative/lister_int_test.go index a316c55613..9a51cff79e 100644 --- a/pkg/knative/lister_int_test.go +++ b/pkg/knative/lister_int_test.go @@ -10,10 +10,11 @@ import ( ) func TestInt_List(t *testing.T) { + kc := defaultKc() listertesting.TestInt_List(t, - knative.NewLister(true), - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewDescriber(true), - knative.NewRemover(true), + knative.NewLister(kc, true), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewDescriber(kc, true), + knative.NewRemover(kc, true), knative.KnativeDeployerName) } diff --git a/pkg/knative/logs.go b/pkg/knative/logs.go index 19790f7d09..02e3ad3e06 100644 --- a/pkg/knative/logs.go +++ b/pkg/knative/logs.go @@ -16,7 +16,7 @@ import ( // The image must be the digest format since pods of Knative service use it. // // This function runs as long as the passed context is active (i.e. it is required cancel the context to stop log gathering). -func GetKServiceLogs(ctx context.Context, namespace, kServiceName, image string, since *time.Time, out io.Writer) error { +func GetKServiceLogs(ctx context.Context, kc *k8s.Client, namespace, kServiceName, image string, since *time.Time, out io.Writer) error { selector := fmt.Sprintf("serving.knative.dev/service=%s", kServiceName) - return k8s.GetPodLogsBySelector(ctx, namespace, selector, "user-container", image, since, out) + return k8s.GetPodLogsBySelector(ctx, kc, namespace, selector, "user-container", image, since, out) } diff --git a/pkg/knative/remover.go b/pkg/knative/remover.go index ed81738407..7d177e3ded 100644 --- a/pkg/knative/remover.go +++ b/pkg/knative/remover.go @@ -8,18 +8,21 @@ import ( apiErrors "k8s.io/apimachinery/pkg/api/errors" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" ) const RemoveTimeout = 120 * time.Second -func NewRemover(verbose bool) *Remover { +func NewRemover(k8sClient *k8s.Client, verbose bool) *Remover { return &Remover{ - verbose: verbose, + verbose: verbose, + k8sClient: k8sClient, } } type Remover struct { - verbose bool + verbose bool + k8sClient *k8s.Client } func (remover *Remover) Remove(ctx context.Context, name, ns string) error { @@ -28,7 +31,7 @@ func (remover *Remover) Remove(ctx context.Context, name, ns string) error { return fn.ErrNamespaceRequired } - client, err := NewServingClient(ns) + client, err := NewServingClient(remover.k8sClient, ns) if err != nil { return err } diff --git a/pkg/knative/remover_int_test.go b/pkg/knative/remover_int_test.go index 6107e96a27..cb95ef798e 100644 --- a/pkg/knative/remover_int_test.go +++ b/pkg/knative/remover_int_test.go @@ -10,10 +10,11 @@ import ( ) func TestInt_Remove(t *testing.T) { + kc := defaultKc() removertesting.TestInt_Remove(t, - knative.NewRemover(true), - knative.NewDeployer(knative.WithDeployerVerbose(true)), - knative.NewDescriber(true), - knative.NewLister(true), + knative.NewRemover(kc, true), + knative.NewDeployer(kc, knative.WithDeployerVerbose(true)), + knative.NewDescriber(kc, true), + knative.NewLister(kc, true), knative.KnativeDeployerName) } diff --git a/pkg/operator/sync.go b/pkg/operator/sync.go index c067abe005..8e1a71f427 100644 --- a/pkg/operator/sync.go +++ b/pkg/operator/sync.go @@ -43,8 +43,8 @@ var ensureRegistrySecret = k8s.EnsureDockerRegistrySecretExist // SyncFunctionCR creates or updates a Function CR for the given function. // It sets up Kubernetes clients, checks if the Function CRD exists on the // cluster, and creates or updates the CR accordingly. -func SyncFunctionCR(ctx context.Context, cfg SyncConfig) error { - restCfg, err := k8s.GetClientConfig().ClientConfig() +func SyncFunctionCR(ctx context.Context, cfg SyncConfig, kc *k8s.Client) error { + restCfg, err := kc.ClientConfig() if err != nil { return fmt.Errorf("getting kubernetes config: %w", err) } @@ -64,10 +64,10 @@ func SyncFunctionCR(ctx context.Context, cfg SyncConfig) error { return fmt.Errorf("creating kubernetes client: %w", err) } - return syncFunctionCR(ctx, cl, disc, cfg) + return syncFunctionCR(ctx, kc, cl, disc, cfg) } -func syncFunctionCR(ctx context.Context, cl ctrlclient.Client, disc discovery.DiscoveryInterface, cfg SyncConfig) error { +func syncFunctionCR(ctx context.Context, kc *k8s.Client, cl ctrlclient.Client, disc discovery.DiscoveryInterface, cfg SyncConfig) error { hasCRD, err := hasFunctionCRD(disc) if err != nil { return fmt.Errorf("checking for Function CRD: %w", err) @@ -84,7 +84,7 @@ func syncFunctionCR(ctx context.Context, cl ctrlclient.Client, disc discovery.Di var registrySecretRef *v1.LocalObjectReference if cfg.RegistryCredentials != nil { secretName := cfg.FunctionName + "-registry-auth" - if err := ensureRegistrySecret(ctx, secretName, cfg.Namespace, nil, nil, + if err := ensureRegistrySecret(ctx, kc, secretName, cfg.Namespace, nil, nil, cfg.RegistryCredentials.Username, cfg.RegistryCredentials.Password, cfg.RegistryCredentials.Server); err != nil { return fmt.Errorf("creating registry secret: %w", err) } diff --git a/pkg/operator/sync_test.go b/pkg/operator/sync_test.go index e59fe8cb9c..f21f636a13 100644 --- a/pkg/operator/sync_test.go +++ b/pkg/operator/sync_test.go @@ -13,6 +13,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" v1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1" + "knative.dev/func/pkg/k8s" ) func newScheme() *runtime.Scheme { @@ -53,7 +54,7 @@ func TestSyncFunctionCR_CreateNew(t *testing.T) { RepoPath: ".", } - err := syncFunctionCR(context.Background(), cl, disc, cfg) + err := syncFunctionCR(context.Background(), nil, cl, disc, cfg) if err != nil { t.Fatal(err) } @@ -102,7 +103,7 @@ func TestSyncFunctionCR_UpdateExistingByMetadataName(t *testing.T) { RepoPath: "subfolder", } - err := syncFunctionCR(context.Background(), cl, disc, cfg) + err := syncFunctionCR(context.Background(), nil, cl, disc, cfg) if err != nil { t.Fatal(err) } @@ -161,7 +162,7 @@ func TestSyncFunctionCR_UpdateExistingByStatusName(t *testing.T) { RepoPath: ".", } - err := syncFunctionCR(context.Background(), cl, disc, cfg) + err := syncFunctionCR(context.Background(), nil, cl, disc, cfg) if err != nil { t.Fatal(err) } @@ -196,7 +197,7 @@ func TestSyncFunctionCR_NoCRD_SkipSilently(t *testing.T) { RepoPath: ".", } - err := syncFunctionCR(context.Background(), cl, disc, cfg) + err := syncFunctionCR(context.Background(), nil, cl, disc, cfg) if err != nil { t.Fatalf("expected no error when CRD missing, got: %v", err) } @@ -204,7 +205,7 @@ func TestSyncFunctionCR_NoCRD_SkipSilently(t *testing.T) { func TestSyncFunctionCR_WithRegistryCredentials(t *testing.T) { original := ensureRegistrySecret - ensureRegistrySecret = func(_ context.Context, _, _ string, _, _ map[string]string, _, _, _ string) error { + ensureRegistrySecret = func(_ context.Context, _ *k8s.Client, _, _ string, _, _ map[string]string, _, _, _ string) error { return nil } t.Cleanup(func() { ensureRegistrySecret = original }) @@ -226,7 +227,7 @@ func TestSyncFunctionCR_WithRegistryCredentials(t *testing.T) { }, } - err := syncFunctionCR(context.Background(), cl, disc, cfg) + err := syncFunctionCR(context.Background(), nil, cl, disc, cfg) if err != nil { t.Fatal(err) } @@ -260,7 +261,7 @@ func TestSyncFunctionCR_NoRepoURL_SkipsWithMessage(t *testing.T) { RepoPath: ".", } - err := syncFunctionCR(context.Background(), cl, disc, cfg) + err := syncFunctionCR(context.Background(), nil, cl, disc, cfg) if err != nil { t.Fatalf("expected no error when repo URL empty, got: %v", err) } diff --git a/pkg/operator/syncer.go b/pkg/operator/syncer.go index a54af32297..bbce751cf7 100644 --- a/pkg/operator/syncer.go +++ b/pkg/operator/syncer.go @@ -4,6 +4,7 @@ import ( "context" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/docker" funcgit "knative.dev/func/pkg/git" @@ -14,10 +15,11 @@ type SyncerOpt func(*Syncer) type Syncer struct { credentialsProvider oci.CredentialsProvider + kc *k8s.Client } -func NewSyncer(opts ...SyncerOpt) *Syncer { - s := &Syncer{} +func NewSyncer(kc *k8s.Client, opts ...SyncerOpt) *Syncer { + s := &Syncer{kc: kc} for _, o := range opts { o(s) } @@ -78,5 +80,5 @@ func (s *Syncer) Sync(ctx context.Context, f fn.Function) error { RepoBranch: repoBranch, RepoPath: repoPath, RegistryCredentials: registryCredentials, - }) + }, s.kc) } diff --git a/pkg/pipelines/tekton/client.go b/pkg/pipelines/tekton/client.go index 399ddc35f2..b884a23f6d 100644 --- a/pkg/pipelines/tekton/client.go +++ b/pkg/pipelines/tekton/client.go @@ -14,8 +14,8 @@ const ( ) // NewTektonClient returns TektonV1beta1Client for namespace -func NewTektonClient(namespace string) (*v1.TektonV1Client, error) { - restConfig, err := k8s.GetClientConfig().ClientConfig() +func NewTektonClient(kc *k8s.Client, namespace string) (*v1.TektonV1Client, error) { + restConfig, err := kc.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to create new tekton client: %w", err) } @@ -28,8 +28,8 @@ func NewTektonClient(namespace string) (*v1.TektonV1Client, error) { return client, nil } -func NewTektonClients() (*cli.Clients, error) { - restConfig, err := k8s.GetClientConfig().ClientConfig() +func NewTektonClients(kc *k8s.Client) (*cli.Clients, error) { + restConfig, err := kc.ClientConfig() if err != nil { return nil, fmt.Errorf("failed to create new tekton clientset: %v", err) } diff --git a/pkg/pipelines/tekton/gitlab_int_test.go b/pkg/pipelines/tekton/gitlab_int_test.go index da99814db6..7992b06af3 100644 --- a/pkg/pipelines/tekton/gitlab_int_test.go +++ b/pkg/pipelines/tekton/gitlab_int_test.go @@ -114,7 +114,8 @@ func TestInt_Gitlab(t *testing.T) { Password: "", }, nil } - pp := tekton.NewPipelinesProvider( + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + pp := tekton.NewPipelinesProvider(kc, tekton.WithCredentialsProvider(credentialsProvider), tekton.WithPacURLCallback(func() (string, error) { return "http://" + pacCtrHostname, nil @@ -609,7 +610,7 @@ func generateSSHKeys(t *testing.T) string { func usingNamespace(t *testing.T) string { name := "gitlab-test-" + strings.ToLower(random.AlphaString(5)) - k8sClient, err := k8s.NewKubernetesClientset() + k8sClient, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } @@ -663,7 +664,8 @@ func usingNamespace(t *testing.T) string { func awaitBuildCompletion(t *testing.T, name, ns string) <-chan struct{} { - clis, err := tekton.NewTektonClients() + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + clis, err := tekton.NewTektonClients(kc) if err != nil { t.Fatal(err) } diff --git a/pkg/pipelines/tekton/pac/client.go b/pkg/pipelines/tekton/pac/client.go index 64687d8c17..bfa7905a9d 100644 --- a/pkg/pipelines/tekton/pac/client.go +++ b/pkg/pipelines/tekton/pac/client.go @@ -9,16 +9,16 @@ import ( ) // NewTektonPacClientAndResolvedNamespace returns PipelinesascodeV1alpha1Client,namespace,error -func NewTektonPacClientAndResolvedNamespace(namespace string) (*pacv1alpha1.PipelinesascodeV1alpha1Client, string, error) { +func NewTektonPacClientAndResolvedNamespace(kc *k8s.Client, namespace string) (*pacv1alpha1.PipelinesascodeV1alpha1Client, string, error) { var err error if namespace == "" { - namespace, err = k8s.GetDefaultNamespace() + namespace, err = kc.DefaultNamespace() if err != nil { return nil, "", err } } - restConfig, err := k8s.GetClientConfig().ClientConfig() + restConfig, err := kc.ClientConfig() if err != nil { return nil, namespace, fmt.Errorf("failed to create new tekton pac client: %w", err) } diff --git a/pkg/pipelines/tekton/pac/pac.go b/pkg/pipelines/tekton/pac/pac.go index 97d7b06a10..0e5c1be626 100644 --- a/pkg/pipelines/tekton/pac/pac.go +++ b/pkg/pipelines/tekton/pac/pac.go @@ -23,15 +23,15 @@ const ( // DetectPACInstallation checks whether PAC is installed on the cluster // Taken and slightly modified from https://github.com/openshift-pipelines/pipelines-as-code/blob/6a7f043f9bb51d04ab729505b26446695595df1f/pkg/cmd/tknpac/bootstrap/bootstrap.go -func DetectPACInstallation(ctx context.Context) (bool, string, error) { +func DetectPACInstallation(ctx context.Context, kc *k8s.Client) (bool, string, error) { var installed bool - clientPac, cns, err := NewTektonPacClientAndResolvedNamespace("") + clientPac, cns, err := NewTektonPacClientAndResolvedNamespace(kc, "") if err != nil { return false, "", err } - clientK8s, _, err := k8s.NewClientAndResolvedNamespace("") + clientK8s, _, err := kc.ClientAndNamespace("") if err != nil { return false, "", err } @@ -68,12 +68,12 @@ func DetectPACInstallation(ctx context.Context) (bool, string, error) { // DetectPACOpenShiftRoute detect the openshift route where the pac controller is running // Taken and slightly modified from https://github.com/openshift-pipelines/pipelines-as-code/blob/0d63e6239f4a7f1fc90decde1e0a154ed56ed0e7/pkg/cmd/tknpac/bootstrap/route.go -func DetectPACOpenShiftRoute(ctx context.Context, targetNamespace string) (string, error) { +func DetectPACOpenShiftRoute(ctx context.Context, kc *k8s.Client, targetNamespace string) (string, error) { gvr := schema.GroupVersionResource{ Group: openShiftRouteGroup, Version: openShiftRouteVersion, Resource: openShiftRouteResource, } - client, err := k8s.NewDynamicClient() + client, err := kc.DynamicClient() if err != nil { return "", err } @@ -105,8 +105,8 @@ func DetectPACOpenShiftRoute(ctx context.Context, targetNamespace string) (strin // GetPACInfo returns the controller url that PAC controller is running // Taken and slightly modified from https://github.com/openshift-pipelines/pipelines-as-code/blob/0d63e6239f4a7f1fc90decde1e0a154ed56ed0e7/pkg/cli/info/configmap.go -func GetPACInfo(ctx context.Context, namespace string) (string, error) { - client, namespace, err := k8s.NewClientAndResolvedNamespace(namespace) +func GetPACInfo(ctx context.Context, kc *k8s.Client, namespace string) (string, error) { + client, namespace, err := kc.ClientAndNamespace(namespace) if err != nil { return "", err } diff --git a/pkg/pipelines/tekton/pipelines_int_test.go b/pkg/pipelines/tekton/pipelines_int_test.go index c0ecf5e87b..5ec825e4b5 100644 --- a/pkg/pipelines/tekton/pipelines_int_test.go +++ b/pkg/pipelines/tekton/pipelines_int_test.go @@ -45,22 +45,23 @@ const ( ) func newRemoteTestClient(verbose bool, deployer string, opts ...fn.Option) *fn.Client { + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) baseOpts := []fn.Option{ fn.WithBuilder(buildpacks.NewBuilder(buildpacks.WithVerbose(verbose))), fn.WithPusher(docker.NewPusher(docker.WithCredentialsProvider(testCP))), - fn.WithDescribers(knative.NewDescriber(verbose), k8s.NewDescriber(verbose), keda.NewDescriber(verbose)), - fn.WithListers(knative.NewLister(verbose), k8s.NewLister(verbose), keda.NewLister(verbose)), - fn.WithRemovers(knative.NewRemover(verbose), k8s.NewRemover(verbose), keda.NewRemover(verbose)), - fn.WithPipelinesProvider(tekton.NewPipelinesProvider(tekton.WithCredentialsProvider(testCP), tekton.WithVerbose(verbose))), + fn.WithDescribers(knative.NewDescriber(kc, verbose), k8s.NewDescriber(kc, verbose), keda.NewDescriber(kc, verbose)), + fn.WithListers(knative.NewLister(kc, verbose), k8s.NewLister(kc, verbose), keda.NewLister(kc, verbose)), + fn.WithRemovers(knative.NewRemover(kc, verbose), k8s.NewRemover(kc, verbose), keda.NewRemover(kc, verbose)), + fn.WithPipelinesProvider(tekton.NewPipelinesProvider(kc, tekton.WithCredentialsProvider(testCP), tekton.WithVerbose(verbose))), } switch deployer { case k8s.KubernetesDeployerName: - baseOpts = append(baseOpts, fn.WithDeployer(k8s.NewDeployer(k8s.WithDeployerVerbose(verbose)))) + baseOpts = append(baseOpts, fn.WithDeployer(k8s.NewDeployer(kc, k8s.WithDeployerVerbose(verbose)))) case keda.KedaDeployerName: - baseOpts = append(baseOpts, fn.WithDeployer(keda.NewDeployer(keda.WithDeployerVerbose(verbose)))) + baseOpts = append(baseOpts, fn.WithDeployer(keda.NewDeployer(kc, keda.WithDeployerVerbose(verbose)))) case knative.KnativeDeployerName: - baseOpts = append(baseOpts, fn.WithDeployer(knative.NewDeployer(knative.WithDeployerVerbose(verbose)))) + baseOpts = append(baseOpts, fn.WithDeployer(knative.NewDeployer(kc, knative.WithDeployerVerbose(verbose)))) } return fn.New(append(baseOpts, opts...)...) @@ -96,7 +97,8 @@ func assertFunctionEchoes(httpClient *http.Client, url string) (err error) { func httpClientForDeployer(t *testing.T, ctx context.Context, deployer string) *http.Client { switch deployer { case k8s.KubernetesDeployerName, keda.KedaDeployerName: - dialer, err := k8s.NewInClusterDialer(ctx, k8s.GetClientConfig()) + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + dialer, err := k8s.NewInClusterDialer(ctx, kc) if err != nil { t.Fatalf("failed to create in-cluster dialer: %v", err) } @@ -194,7 +196,7 @@ func TestInt_Remote_Default(t *testing.T) { func setupNS(t *testing.T) string { name := "pipeline-integration-test-" + strings.ToLower(random.AlphaString(5)) - cliSet, err := k8s.NewKubernetesClientset() + cliSet, err := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})).Clientset() if err != nil { t.Fatal(err) } diff --git a/pkg/pipelines/tekton/pipelines_pac_provider.go b/pkg/pipelines/tekton/pipelines_pac_provider.go index 9c007f274c..2bffb4383d 100644 --- a/pkg/pipelines/tekton/pipelines_pac_provider.go +++ b/pkg/pipelines/tekton/pipelines_pac_provider.go @@ -53,7 +53,7 @@ func (pp *PipelinesProvider) ConfigurePAC(ctx context.Context, f fn.Function, me data.WebhookSecret = random.AlphaString(10) // try to reuse existing Webhook Secret stored in the cluster - secret, err := k8s.GetSecret(ctx, getPipelineSecretName(f), namespace) + secret, err := k8s.GetSecret(ctx, pp.kc, getPipelineSecretName(f), namespace) if err != nil { if !k8serrors.IsNotFound(err) { return err @@ -147,7 +147,7 @@ func (pp *PipelinesProvider) createClusterPACResources(ctx context.Context, f fn } // figure out pac installation namespace - installed, _, err := pac.DetectPACInstallation(ctx) + installed, _, err := pac.DetectPACInstallation(ctx, pp.kc) if !installed { errMsg := "" if err != nil { @@ -191,19 +191,19 @@ func (pp *PipelinesProvider) createClusterPACResources(ctx context.Context, f fn metadata.RegistryPassword = creds.Password metadata.RegistryServer = registry - err = createPipelinePersistentVolumeClaim(ctx, f, namespace, labels) + err = createPipelinePersistentVolumeClaim(ctx, pp.kc, f, namespace, labels) if err != nil { return err } fmt.Printf(" ✅ Persistent Volume is present on the cluster with name %q\n", getPipelinePvcName(f)) - err = ensurePACSecretExists(ctx, f, namespace, metadata, labels) + err = ensurePACSecretExists(ctx, pp.kc, f, namespace, metadata, labels) if err != nil { return err } fmt.Printf(" ✅ Credentials are present on the cluster in secret %q\n", getPipelineSecretName(f)) - err = ensurePACRepositoryExists(ctx, f, namespace, metadata, labels) + err = ensurePACRepositoryExists(ctx, pp.kc, f, namespace, metadata, labels) if err != nil { return err } @@ -218,7 +218,7 @@ func (pp *PipelinesProvider) createClusterPACResources(ctx context.Context, f fn func (pp *PipelinesProvider) createRemotePACResources(ctx context.Context, f fn.Function, metadata pipelines.PacMetadata) error { // figure out pac installation namespace - installed, installationNS, err := pac.DetectPACInstallation(ctx) + installed, installationNS, err := pac.DetectPACInstallation(ctx, pp.kc) if !installed { errMsg := "" if err != nil { @@ -231,14 +231,14 @@ func (pp *PipelinesProvider) createRemotePACResources(ctx context.Context, f fn. } // fetch configmap to get controller url - controllerURL, err := pac.GetPACInfo(ctx, installationNS) + controllerURL, err := pac.GetPACInfo(ctx, pp.kc, installationNS) if err != nil { return err } // check if info configmap has url then use that otherwise try to detect if controllerURL == "" { - controllerURL, _ = pac.DetectPACOpenShiftRoute(ctx, installationNS) + controllerURL, _ = pac.DetectPACOpenShiftRoute(ctx, pp.kc, installationNS) } // we haven't been able to detect PAC controller public route, let's prompt: diff --git a/pkg/pipelines/tekton/pipelines_pac_provider_test.go b/pkg/pipelines/tekton/pipelines_pac_provider_test.go index 8fe536f6a1..b5f76298da 100644 --- a/pkg/pipelines/tekton/pipelines_pac_provider_test.go +++ b/pkg/pipelines/tekton/pipelines_pac_provider_test.go @@ -50,7 +50,7 @@ func Test_createLocalResources(t *testing.T) { f.Image = "docker.io/alice/" + f.Name f.Registry = TestRegistry - pp := NewPipelinesProvider() + pp := NewPipelinesProvider(nil) err = pp.createLocalPACResources(t.Context(), f) if (err != nil) != tt.wantErr { t.Errorf("pp.createLocalResources() error = %v, wantErr %v", err, tt.wantErr) @@ -74,7 +74,7 @@ func Test_deleteAllPipelineTemplates(t *testing.T) { f.Image = "docker.io/alice/" + f.Name f.Registry = TestRegistry - pp := NewPipelinesProvider() + pp := NewPipelinesProvider(nil) err = pp.createLocalPACResources(t.Context(), f) if err != nil { t.Errorf("unexpected error while running pp.createLocalResources() error = %v", err) diff --git a/pkg/pipelines/tekton/pipelines_provider.go b/pkg/pipelines/tekton/pipelines_provider.go index b76e863b4f..f4b065520a 100644 --- a/pkg/pipelines/tekton/pipelines_provider.go +++ b/pkg/pipelines/tekton/pipelines_provider.go @@ -58,6 +58,7 @@ type PipelinesProvider struct { credentialsProvider oci.CredentialsProvider decorator PipelineDecorator transport http.RoundTripper + kc *k8s.Client } func WithCredentialsProvider(credentialsProvider oci.CredentialsProvider) Opt { @@ -90,8 +91,9 @@ func WithPacURLCallback(getPacURL pacURLCallback) Opt { } } -func NewPipelinesProvider(opts ...Opt) *PipelinesProvider { +func NewPipelinesProvider(kc *k8s.Client, opts ...Opt) *PipelinesProvider { pp := &PipelinesProvider{ + kc: kc, getPacURL: func() (string, error) { var url string e := survey.AskOne(&survey.Input{ @@ -151,7 +153,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn f.Deploy.Image = image // Client for the given namespace - client, err := NewTektonClient(namespace) + client, err := NewTektonClient(pp.kc, namespace) if err != nil { return "", f, err } @@ -165,7 +167,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn labels = pp.decorator.UpdateLabels(f, labels) } - err = createPipelinePersistentVolumeClaim(ctx, f, namespace, labels) + err = createPipelinePersistentVolumeClaim(ctx, pp.kc, f, namespace, labels) if err != nil { return "", f, err } @@ -174,13 +176,13 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn // Use direct upload to PVC if Git is not set up. content := sourcesAsTarStream(f) defer content.Close() - err = k8s.UploadToVolume(ctx, content, getPipelinePvcName(f), namespace) + err = k8s.UploadToVolume(ctx, pp.kc, content, getPipelinePvcName(f), namespace) if err != nil { return "", f, fmt.Errorf("cannot upload sources to the PVC: %w", err) } } - err = createAndApplyPipelineTemplate(f, namespace, labels) + err = createAndApplyPipelineTemplate(pp.kc, f, namespace, labels) if err != nil { if !k8serrors.IsAlreadyExists(err) { if k8serrors.IsNotFound(err) { @@ -212,12 +214,12 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn f.Registry = registry } - err = k8s.EnsureDockerRegistrySecretExist(ctx, getPipelineSecretName(f), namespace, labels, f.Deploy.Annotations, creds.Username, creds.Password, registry) + err = k8s.EnsureDockerRegistrySecretExist(ctx, pp.kc, getPipelineSecretName(f), namespace, labels, f.Deploy.Annotations, creds.Username, creds.Password, registry) if err != nil { return "", f, fmt.Errorf("problem in creating secret: %v", err) } - err = createAndApplyPipelineRunTemplate(f, namespace, labels) + err = createAndApplyPipelineRunTemplate(pp.kc, f, namespace, labels) if err != nil { return "", f, fmt.Errorf("problem in creating pipeline run: %v", err) } @@ -246,19 +248,19 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) (string, fn } if newestPipelineRun.Status.GetCondition(apis.ConditionSucceeded).Status == corev1.ConditionFalse { - message := getFailedPipelineRunLog(ctx, client, newestPipelineRun, namespace) + message := getFailedPipelineRunLog(ctx, pp.kc, client, newestPipelineRun, namespace) return "", f, fmt.Errorf("function pipeline run has failed with message: \n\n%s", message) } var describer fn.Describer switch f.Deploy.Deployer { case k8s.KubernetesDeployerName: - describer = k8s.NewDescriber(false, k8s.WithDescriberTransport(pp.transport)) + describer = k8s.NewDescriber(pp.kc, false, k8s.WithDescriberTransport(pp.transport)) case keda.KedaDeployerName: - describer = keda.NewDescriber(false, keda.WithDescriberTransport(pp.transport)) + describer = keda.NewDescriber(pp.kc, false, keda.WithDescriberTransport(pp.transport)) default: // default to knative - describer = knative.NewDescriber(false, knative.WithDescriberTransport(pp.transport)) + describer = knative.NewDescriber(pp.kc, false, knative.WithDescriberTransport(pp.transport)) } obj, err := describer.Describe(ctx, f.Name, f.Namespace) @@ -415,7 +417,7 @@ func (pp *PipelinesProvider) removeClusterResources(ctx context.Context, f fn.Fu // let's try to delete all resources in parallel, so the operation doesn't take long wg := sync.WaitGroup{} - deleteFunctions := []func(context.Context, string, metav1.ListOptions) error{ + deleteFunctions := []func(context.Context, *k8s.Client, string, metav1.ListOptions) error{ deletePipelines, deletePipelineRuns, k8s.DeleteSecrets, @@ -430,7 +432,7 @@ func (pp *PipelinesProvider) removeClusterResources(ctx context.Context, f fn.Fu df := deleteFunctions[i] go func() { defer wg.Done() - err := df(ctx, namespace, listOptions) + err := df(ctx, pp.kc, namespace, listOptions) if err != nil && !k8serrors.IsNotFound(err) && !k8serrors.IsForbidden(err) { errChan <- err } @@ -464,7 +466,7 @@ func (pp *PipelinesProvider) watchPipelineRunProgress(ctx context.Context, pr *v "deploy": "Deploying function to the cluster", } - clients, err := NewTektonClients() + clients, err := NewTektonClients(pp.kc) if err != nil { return err } @@ -511,7 +513,7 @@ out: // getFailedPipelineRunLog returns log message for a failed PipelineRun, // returns log from a container where the failing TaskRun is running, if available. -func getFailedPipelineRunLog(ctx context.Context, client *pipelineClient.TektonV1Client, pr *v1.PipelineRun, namespace string) string { +func getFailedPipelineRunLog(ctx context.Context, kc *k8s.Client, client *pipelineClient.TektonV1Client, pr *v1.PipelineRun, namespace string) string { // Reason "Failed" usually means there is a specific failure in some step, // let's find the failed step and try to get log directly from the container. // If we are not able to get the container's log, we return the generic message from the PipelineRun.Status. @@ -526,7 +528,7 @@ func getFailedPipelineRunLog(ctx context.Context, client *pipelineClient.TektonV for _, s := range t.Status.Steps { // let's try to print logs of the first unsuccessful step if s.Terminated != nil && s.Terminated.ExitCode != 0 { - podLogs, err := k8s.GetPodLogs(ctx, namespace, t.Status.PodName, s.Container) + podLogs, err := k8s.GetPodLogs(ctx, kc, namespace, t.Status.PodName, s.Container) if err == nil { return podLogs } @@ -582,24 +584,24 @@ var getPersistentVolumeClaim = k8s.GetPersistentVolumeClaim var deletePersistentVolumeClaim = k8s.DeletePersistentVolumeClaim var waitForPVCDeletion = k8s.WaitForPVCDeletion -func createPipelinePersistentVolumeClaim(ctx context.Context, f fn.Function, namespace string, labels map[string]string) error { +func createPipelinePersistentVolumeClaim(ctx context.Context, kc *k8s.Client, f fn.Function, namespace string, labels map[string]string) error { pvcName := getPipelinePvcName(f) // Check if PVC already exists - existingPVC, err := getPersistentVolumeClaim(ctx, pvcName, namespace) + existingPVC, err := getPersistentVolumeClaim(ctx, kc, pvcName, namespace) if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("failed to check existing PVC: %w", err) } // If PVC exists, delete it and wait for full deletion to ensure clean workspace if existingPVC != nil { - err = deletePersistentVolumeClaim(ctx, pvcName, namespace) + err = deletePersistentVolumeClaim(ctx, kc, pvcName, namespace) if err != nil { return fmt.Errorf("failed to delete existing PVC: %w", err) } // Wait for PVC to be fully deleted (not just Terminating) - err = waitForPVCDeletion(ctx, pvcName, namespace) + err = waitForPVCDeletion(ctx, kc, pvcName, namespace) if err != nil { return fmt.Errorf("failed waiting for PVC deletion: %w", err) } @@ -614,7 +616,7 @@ func createPipelinePersistentVolumeClaim(ctx context.Context, f fn.Function, nam } } - err = createPersistentVolumeClaim(ctx, pvcName, namespace, labels, f.Deploy.Annotations, corev1.ReadWriteOnce, pvcs, f.Build.RemoteStorageClass) + err = createPersistentVolumeClaim(ctx, kc, pvcName, namespace, labels, f.Deploy.Annotations, corev1.ReadWriteOnce, pvcs, f.Build.RemoteStorageClass) if err != nil { return fmt.Errorf("problem creating persistent volume claim: %w", err) } diff --git a/pkg/pipelines/tekton/pipelines_provider_test.go b/pkg/pipelines/tekton/pipelines_provider_test.go index 61c2c1f349..d72ecaf032 100644 --- a/pkg/pipelines/tekton/pipelines_provider_test.go +++ b/pkg/pipelines/tekton/pipelines_provider_test.go @@ -16,6 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" ) func TestSourcesAsTarStream(t *testing.T) { @@ -82,13 +83,14 @@ func TestSourcesAsTarStream(t *testing.T) { } func Test_createPipelinePersistentVolumeClaim(t *testing.T) { - type mockCreateType func(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClass string) (err error) - type mockGetType func(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) - type mockDeleteType func(ctx context.Context, name, namespaceOverride string) error - type mockWaitType func(ctx context.Context, name, namespaceOverride string) error + type mockCreateType func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClass string) (err error) + type mockGetType func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) + type mockDeleteType func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error + type mockWaitType func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error type args struct { ctx context.Context + kc *k8s.Client f fn.Function namespace string labels map[string]string @@ -112,10 +114,10 @@ func Test_createPipelinePersistentVolumeClaim(t *testing.T) { labels: nil, size: DefaultPersistentVolumeClaimSize.String(), }, - mockGet: func(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { + mockGet: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { return nil, &apiErrors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}} }, - mockCreate: func(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClass string) (err error) { + mockCreate: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClass string) (err error) { return errors.New("creation of pvc failed") }, wantErr: true, @@ -129,16 +131,16 @@ func Test_createPipelinePersistentVolumeClaim(t *testing.T) { labels: nil, size: DefaultPersistentVolumeClaimSize.String(), }, - mockGet: func(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { + mockGet: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { return &corev1.PersistentVolumeClaim{}, nil }, - mockDelete: func(ctx context.Context, name, namespaceOverride string) error { + mockDelete: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error { return nil }, - mockWait: func(ctx context.Context, name, namespaceOverride string) error { + mockWait: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error { return nil }, - mockCreate: func(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClass string) (err error) { + mockCreate: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity, storageClass string) (err error) { return nil }, wantErr: false, @@ -147,15 +149,16 @@ func Test_createPipelinePersistentVolumeClaim(t *testing.T) { name: "returns error if deletion fails", args: args{ ctx: t.Context(), + kc: k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})), f: fn.Function{}, namespace: "test-ns", labels: nil, size: DefaultPersistentVolumeClaimSize.String(), }, - mockGet: func(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { + mockGet: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { return &corev1.PersistentVolumeClaim{}, nil }, - mockDelete: func(ctx context.Context, name, namespaceOverride string) error { + mockDelete: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error { return errors.New("deletion failed") }, wantErr: true, @@ -169,13 +172,13 @@ func Test_createPipelinePersistentVolumeClaim(t *testing.T) { labels: nil, size: DefaultPersistentVolumeClaimSize.String(), }, - mockGet: func(ctx context.Context, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { + mockGet: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) (*corev1.PersistentVolumeClaim, error) { return &corev1.PersistentVolumeClaim{}, nil }, - mockDelete: func(ctx context.Context, name, namespaceOverride string) error { + mockDelete: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error { return nil }, - mockWait: func(ctx context.Context, name, namespaceOverride string) error { + mockWait: func(ctx context.Context, kc *k8s.Client, name, namespaceOverride string) error { return errors.New("wait for deletion failed") }, wantErr: true, @@ -209,7 +212,7 @@ func Test_createPipelinePersistentVolumeClaim(t *testing.T) { } tt.args.f.Build.PVCSize = tt.args.size - if err := createPipelinePersistentVolumeClaim(tt.args.ctx, tt.args.f, tt.args.namespace, tt.args.labels); (err != nil) != tt.wantErr { + if err := createPipelinePersistentVolumeClaim(tt.args.ctx, tt.args.kc, tt.args.f, tt.args.namespace, tt.args.labels); (err != nil) != tt.wantErr { t.Errorf("createPipelinePersistentVolumeClaim() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkg/pipelines/tekton/resources.go b/pkg/pipelines/tekton/resources.go index be9c36b10a..86d7ec7a0e 100644 --- a/pkg/pipelines/tekton/resources.go +++ b/pkg/pipelines/tekton/resources.go @@ -12,14 +12,15 @@ import ( "knative.dev/func/pkg/builders" "knative.dev/func/pkg/buildpacks" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/s2i" ) -func deletePipelines(ctx context.Context, namespace string, listOptions metav1.ListOptions) (err error) { +func deletePipelines(ctx context.Context, kc *k8s.Client, namespace string, listOptions metav1.ListOptions) (err error) { if namespace == "" { return errors.New("delete pipeline: namespace required") } - client, err := NewTektonClient(namespace) + client, err := NewTektonClient(kc, namespace) if err != nil { return } @@ -27,11 +28,11 @@ func deletePipelines(ctx context.Context, namespace string, listOptions metav1.L return client.Pipelines(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions) } -func deletePipelineRuns(ctx context.Context, namespace string, listOptions metav1.ListOptions) (err error) { +func deletePipelineRuns(ctx context.Context, kc *k8s.Client, namespace string, listOptions metav1.ListOptions) (err error) { if namespace == "" { return errors.New("delete pipeline run: namespace required") } - client, err := NewTektonClient(namespace) + client, err := NewTektonClient(kc, namespace) if err != nil { return } diff --git a/pkg/pipelines/tekton/resources_pac.go b/pkg/pipelines/tekton/resources_pac.go index cff5fa33e3..8cce17953b 100644 --- a/pkg/pipelines/tekton/resources_pac.go +++ b/pkg/pipelines/tekton/resources_pac.go @@ -17,7 +17,7 @@ import ( ) // ensurePACSecretExists checks that up-to-date secret holding credentials needed for PAC is on the cluster -func ensurePACSecretExists(ctx context.Context, f fn.Function, namespace string, credentials pipelines.PacMetadata, labels map[string]string) error { +func ensurePACSecretExists(ctx context.Context, kc *k8s.Client, f fn.Function, namespace string, credentials pipelines.PacMetadata, labels map[string]string) error { dockerConfigJSONContent, err := k8s.HandleDockerCfgJSONContent(credentials.RegistryUsername, credentials.RegistryPassword, "", credentials.RegistryServer) if err != nil { return err @@ -37,12 +37,12 @@ func ensurePACSecretExists(ctx context.Context, f fn.Function, namespace string, secret.Data["provider.token"] = []byte(credentials.PersonalAccessToken) secret.Data["webhook.secret"] = []byte(credentials.WebhookSecret) - return k8s.EnsureSecretExist(ctx, secret, namespace) + return k8s.EnsureSecretExist(ctx, kc, secret, namespace) } // ensurePACRepositoryExists checks that up-to-date Repository CR is present on the cluster -func ensurePACRepositoryExists(ctx context.Context, f fn.Function, namespace string, metadata pipelines.PacMetadata, labels map[string]string) error { - client, namespace, err := pac.NewTektonPacClientAndResolvedNamespace(namespace) +func ensurePACRepositoryExists(ctx context.Context, kc *k8s.Client, f fn.Function, namespace string, metadata pipelines.PacMetadata, labels map[string]string) error { + client, namespace, err := pac.NewTektonPacClientAndResolvedNamespace(kc, namespace) if err != nil { return err } @@ -100,8 +100,8 @@ func ensurePACRepositoryExists(ctx context.Context, f fn.Function, namespace str } // deletePACRepositories deletes all Repository resources present on the cluster that match input list options -func deletePACRepositories(ctx context.Context, namespaceOverride string, listOptions metav1.ListOptions) error { - client, namespace, err := pac.NewTektonPacClientAndResolvedNamespace(namespaceOverride) +func deletePACRepositories(ctx context.Context, kc *k8s.Client, namespaceOverride string, listOptions metav1.ListOptions) error { + client, namespace, err := pac.NewTektonPacClientAndResolvedNamespace(kc, namespaceOverride) if err != nil { return err } diff --git a/pkg/pipelines/tekton/templates.go b/pkg/pipelines/tekton/templates.go index b853194bcf..c0e9ac301e 100644 --- a/pkg/pipelines/tekton/templates.go +++ b/pkg/pipelines/tekton/templates.go @@ -301,7 +301,7 @@ func getTaskSpec(taskYaml string) (string, error) { // createAndApplyPipelineTemplate creates and applies Pipeline template for a standard on-cluster build // all resources are created on the fly, if there's a Pipeline defined in the project directory, it is used instead -func createAndApplyPipelineTemplate(f fn.Function, namespace string, labels map[string]string) error { +func createAndApplyPipelineTemplate(kc *k8s.Client, f fn.Function, namespace string, labels map[string]string) error { // If Git is set up create fetch task and reference it from build task, // otherwise sources have been already uploaded to workspace PVC. @@ -344,12 +344,12 @@ func createAndApplyPipelineTemplate(f fn.Function, namespace string, labels map[ return builders.ErrBuilderNotSupported{Builder: f.Build.Builder} } - return createAndApplyResource(f.Root, pipelineFileName, template, "pipeline", getPipelineName(f), namespace, data) + return createAndApplyResource(kc, f.Root, pipelineFileName, template, "pipeline", getPipelineName(f), namespace, data) } // createAndApplyPipelineRunTemplate creates and applies PipelineRun template for a standard on-cluster build // all resources are created on the fly, if there's a PipelineRun defined in the project directory, it is used instead -func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels map[string]string) error { +func createAndApplyPipelineRunTemplate(kc *k8s.Client, f fn.Function, namespace string, labels map[string]string) error { contextDir := f.Build.Git.ContextDir if contextDir == "" && f.Build.Builder == builders.S2I { // TODO(lkingland): could instead update S2I to interpret empty string @@ -424,7 +424,7 @@ func createAndApplyPipelineRunTemplate(f fn.Function, namespace string, labels m return builders.ErrBuilderNotSupported{Builder: f.Build.Builder} } - return createAndApplyResource(f.Root, pipelineFileName, template, "pipelinerun", getPipelineRunGenerateName(f), namespace, data) + return createAndApplyResource(kc, f.Root, pipelineFileName, template, "pipelinerun", getPipelineRunGenerateName(f), namespace, data) } // allows simple mocking in unit tests @@ -432,7 +432,7 @@ var manifestivalClient = k8s.GetManifestivalClient // createAndApplyResource tries to create and apply a resource to the k8s cluster from the input template and data, // if there's the same resource already created in the project directory, it is used instead -func createAndApplyResource(projectRoot, fileName, fileTemplate, kind, resourceName, namespace string, data interface{}) error { +func createAndApplyResource(kc *k8s.Client, projectRoot, fileName, fileTemplate, kind, resourceName, namespace string, data interface{}) error { var source manifestival.Source filePath := path.Join(projectRoot, resourcesDirectory, fileName) @@ -452,7 +452,7 @@ func createAndApplyResource(projectRoot, fileName, fileTemplate, kind, resourceN source = manifestival.Reader(&buf) } - client, err := manifestivalClient() + client, err := manifestivalClient(kc) if err != nil { return fmt.Errorf("error generating template: %v", err) } diff --git a/pkg/pipelines/tekton/templates_int_test.go b/pkg/pipelines/tekton/templates_int_test.go index 406c9e746c..8f02cd335c 100644 --- a/pkg/pipelines/tekton/templates_int_test.go +++ b/pkg/pipelines/tekton/templates_int_test.go @@ -9,6 +9,7 @@ import ( "github.com/manifestival/manifestival/fake" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" . "knative.dev/func/pkg/testing" ) @@ -19,7 +20,7 @@ func TestInt_createAndApplyPipelineTemplate(t *testing.T) { old := manifestivalClient defer func() { manifestivalClient = old }() - manifestivalClient = func() (manifestival.Client, error) { + manifestivalClient = func(_ *k8s.Client) (manifestival.Client, error) { return fake.New(), nil } @@ -36,7 +37,7 @@ func TestInt_createAndApplyPipelineTemplate(t *testing.T) { f.Image = "docker.io/alice/" + f.Name f.Registry = TestRegistry - if err := createAndApplyPipelineTemplate(f, tt.namespace, tt.labels); (err != nil) != tt.wantErr { + if err := createAndApplyPipelineTemplate(nil, f, tt.namespace, tt.labels); (err != nil) != tt.wantErr { t.Errorf("createAndApplyPipelineTemplate() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkg/pipelines/tekton/templates_test.go b/pkg/pipelines/tekton/templates_test.go index 6624540d45..1bc1098cb1 100644 --- a/pkg/pipelines/tekton/templates_test.go +++ b/pkg/pipelines/tekton/templates_test.go @@ -11,6 +11,7 @@ import ( "knative.dev/func/pkg/builders" fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" . "knative.dev/func/pkg/testing" ) @@ -301,7 +302,7 @@ func Test_createAndApplyPipelineRunTemplate(t *testing.T) { old := manifestivalClient defer func() { manifestivalClient = old }() - manifestivalClient = func() (manifestival.Client, error) { + manifestivalClient = func(_ *k8s.Client) (manifestival.Client, error) { return fake.New(), nil } @@ -318,7 +319,7 @@ func Test_createAndApplyPipelineRunTemplate(t *testing.T) { f.Image = "docker.io/alice/" + f.Name f.Registry = TestRegistry - if err := createAndApplyPipelineRunTemplate(f, tt.namespace, tt.labels); (err != nil) != tt.wantErr { + if err := createAndApplyPipelineRunTemplate(nil, f, tt.namespace, tt.labels); (err != nil) != tt.wantErr { t.Errorf("createAndApplyPipelineRunTemplate() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkg/testing/k8s/testing.go b/pkg/testing/k8s/testing.go index 954c564fa1..d18bd24bc2 100644 --- a/pkg/testing/k8s/testing.go +++ b/pkg/testing/k8s/testing.go @@ -7,6 +7,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) @@ -17,7 +18,8 @@ const DefaultIntTestNamespacePrefix = "func-int-test" func Namespace(t *testing.T, ctx context.Context) string { t.Helper() - cliSet, err := k8s.NewKubernetesClientset() + kc := k8s.NewClient(k8s.BuildClientConfig("", "", fn.Local{})) + cliSet, err := kc.Clientset() if err != nil { t.Fatal(err) } diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json index 47e2e1b2b2..b39500890b 100644 --- a/schema/func_yaml-schema.json +++ b/schema/func_yaml-schema.json @@ -72,6 +72,10 @@ "type": "string", "description": "Namespace into which the function was deployed on supported platforms." }, + "cluster": { + "type": "string", + "description": "Cluster is the cluster server api URL where the function is deployed" + }, "image": { "type": "string", "description": "Image is the deployed image including sha256"