add git ops options (#1122)

* add git ops options

* add git ops options

* add unit tests

* add unit tests

* add unit tests

* add unit test

* add unit test

* add unit test

* review comments

* review comments

* add single file mode

* add single file mode

* add single file mode

* add single file mode

* review comments
This commit is contained in:
Murugappan Chetty 2021-01-14 04:46:31 -08:00 committed by GitHub
parent 174e41b628
commit f5ac4413d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 592 additions and 33 deletions

View File

@ -48,6 +48,11 @@ kn service create NAME --image IMAGE
# [https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/]
# [https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/]
kn service create s4gpu --image knativesamples/hellocuda-go --request memory=250Mi,cpu=200m --limit nvidia.com/gpu=1
# Create the service in offline mode instead of kubernetes cluster
kn service create gitopstest -n test-ns --image knativesamples/helloworld --target=/user/knfiles
kn service create gitopstest --image knativesamples/helloworld --target=/user/knfiles/test.yaml
kn service create gitopstest --image knativesamples/helloworld --target=/user/knfiles/test.json
```
### Options
@ -88,6 +93,7 @@ kn service create NAME --image IMAGE
--scale-max int Maximum number of replicas.
--scale-min int Minimum number of replicas.
--service-account string Service account name to set. An empty argument ("") clears the service account. The referenced service account must exist in the service's namespace.
--target string work on local directory instead of a remote cluster
--user int The user ID to run the container (e.g., 1001).
--volume stringArray Add a volume from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret: or sc:). Example: --volume myvolume=cm:myconfigmap or --volume myvolume=secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-.
--wait Wait for 'service create' operation to be completed. (default true)

View File

@ -22,6 +22,11 @@ kn service delete NAME [NAME ...]
# Delete all services in 'ns1' namespace
kn service delete --all -n ns1
# Delete the services in offline mode instead of kubernetes cluster
kn service delete test -n test-ns --target=/user/knfiles
kn service delete test --target=/user/knfiles/test.yaml
kn service delete test --target=/user/knfiles/test.json
```
### Options
@ -31,6 +36,7 @@ kn service delete NAME [NAME ...]
-h, --help help for delete
-n, --namespace string Specify the namespace to operate in.
--no-wait Do not wait for 'service delete' operation to be completed. (default true)
--target string work on local directory instead of a remote cluster
--wait Wait for 'service delete' operation to be completed.
--wait-timeout int Seconds to wait before giving up on waiting for service to be deleted. (default 600)
```

View File

@ -22,6 +22,11 @@ kn service describe NAME
# Print only service URL
kn service describe svc -o url
# Describe the services in offline mode instead of kubernetes cluster
kn service describe test -n test-ns --target=/user/knfiles
kn service describe test --target=/user/knfiles/test.yaml
kn service describe test --target=/user/knfiles/test.json
```
### Options
@ -31,6 +36,7 @@ kn service describe NAME
-h, --help help for describe
-n, --namespace string Specify the namespace to operate in.
-o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file|url.
--target string work on local directory instead of a remote cluster
--template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].
-v, --verbose More output.
```

View File

@ -22,6 +22,12 @@ kn service list
# List service 'web'
kn service list web
# List the services in offline mode instead of kubernetes cluster
kn service list --target=/user/knfiles
kn service list --target=/user/knfiles/test.json
kn service list --target=/user/knfiles/test.yaml
kn service list -n test-ns --target=/user/knfiles
```
### Options
@ -33,6 +39,7 @@ kn service list
-n, --namespace string Specify the namespace to operate in.
--no-headers When using the default output format, don't print headers (default: print headers).
-o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file.
--target string work on local directory instead of a remote cluster
--template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].
```

View File

@ -33,6 +33,11 @@ kn service update NAME
# Add tag 'test' to echo-v3 revision with 10% traffic and rest to latest ready revision of service
kn service update svc --tag echo-v3=test --traffic test=10,@latest=90
# Update the service in offline mode instead of kubernetes cluster
kn service update gitopstest -n test-ns --env KEY1=VALUE1 --target=/user/knfiles
kn service update gitopstest --env KEY1=VALUE1 --target=/user/knfiles/test.yaml
kn service update gitopstest --env KEY1=VALUE1 --target=/user/knfiles/test.json
```
### Options
@ -72,6 +77,7 @@ kn service update NAME
--scale-min int Minimum number of replicas.
--service-account string Service account name to set. An empty argument ("") clears the service account. The referenced service account must exist in the service's namespace.
--tag strings Set tag (format: --tag revisionRef=tagName) where revisionRef can be a revision or '@latest' string representing latest ready revision. This flag can be specified multiple times.
--target string work on local directory instead of a remote cluster
--traffic strings Set traffic distribution (format: --traffic revisionRef=percent) where revisionRef can be a revision or a tag or '@latest' string representing latest ready revision. This flag can be given multiple times with percent summing up to 100%.
--untag strings Untag revision (format: --untag tagName). This flag can be specified multiple times.
--user int The user ID to run the container (e.g., 1001).

View File

@ -19,6 +19,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration"
hprinters "knative.dev/client/pkg/printers"
@ -113,3 +114,8 @@ func TranslateTimestampSince(timestamp metav1.Time) string {
}
return duration.HumanDuration(time.Since(timestamp.Time))
}
// AddGitOpsFlags adds flags to enable gitops mode
func AddGitOpsFlags(flags *pflag.FlagSet) {
flags.String("target", "", "work on local directory instead of a remote cluster")
}

View File

@ -95,7 +95,7 @@ func NewServiceApplyCommand(p *commands.KnParams) *cobra.Command {
return showUrl(client, service.Name, "unchanged", "", cmd.OutOrStdout())
}
return waitIfRequested(client, service.Name, waitFlags, waitDoing, waitVerb, cmd.OutOrStdout())
return waitIfRequested(client, waitFlags, service.Name, waitDoing, waitVerb, "", cmd.OutOrStdout())
},
}
commands.AddNamespaceFlags(serviceApplyCommand.Flags(), false)

View File

@ -70,7 +70,12 @@ var create_example = `
# Create a service with 250MB memory, 200m CPU requests and a GPU resource limit
# [https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/]
# [https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/]
kn service create s4gpu --image knativesamples/hellocuda-go --request memory=250Mi,cpu=200m --limit nvidia.com/gpu=1`
kn service create s4gpu --image knativesamples/hellocuda-go --request memory=250Mi,cpu=200m --limit nvidia.com/gpu=1
# Create the service in offline mode instead of kubernetes cluster
kn service create gitopstest -n test-ns --image knativesamples/helloworld --target=/user/knfiles
kn service create gitopstest --image knativesamples/helloworld --target=/user/knfiles/test.yaml
kn service create gitopstest --image knativesamples/helloworld --target=/user/knfiles/test.json`
func NewServiceCreateCommand(p *commands.KnParams) *cobra.Command {
var editFlags ConfigurationEditFlags
@ -106,8 +111,8 @@ func NewServiceCreateCommand(p *commands.KnParams) *cobra.Command {
if err != nil {
return err
}
client, err := p.NewServingClient(namespace)
targetFlag := cmd.Flag("target").Value.String()
client, err := newServingClient(p, namespace, targetFlag)
if err != nil {
return err
}
@ -123,9 +128,9 @@ func NewServiceCreateCommand(p *commands.KnParams) *cobra.Command {
"cannot create service '%s' in namespace '%s' "+
"because the service already exists and no --force option was given", service.Name, namespace)
}
err = replaceService(client, service, waitFlags, out)
err = replaceService(client, service, waitFlags, out, targetFlag)
} else {
err = createService(client, service, waitFlags, out)
err = createService(client, service, waitFlags, out, targetFlag)
}
if err != nil {
return err
@ -134,34 +139,34 @@ func NewServiceCreateCommand(p *commands.KnParams) *cobra.Command {
},
}
commands.AddNamespaceFlags(serviceCreateCommand.Flags(), false)
commands.AddGitOpsFlags(serviceCreateCommand.Flags())
editFlags.AddCreateFlags(serviceCreateCommand)
waitFlags.AddConditionWaitFlags(serviceCreateCommand, commands.WaitDefaultTimeout, "create", "service", "ready")
return serviceCreateCommand
}
func createService(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, out io.Writer) error {
func createService(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, out io.Writer, targetFlag string) error {
err := client.CreateService(service)
if err != nil {
return err
}
return waitIfRequested(client, service.Name, waitFlags, "Creating", "created", out)
return waitIfRequested(client, waitFlags, service.Name, "Creating", "created", targetFlag, out)
}
func replaceService(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, out io.Writer) error {
func replaceService(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, out io.Writer, targetFlag string) error {
err := prepareAndUpdateService(client, service)
if err != nil {
return err
}
return waitIfRequested(client, service.Name, waitFlags, "Replacing", "replaced", out)
return waitIfRequested(client, waitFlags, service.Name, "Replacing", "replaced", targetFlag, out)
}
func waitIfRequested(client clientservingv1.KnServingClient, serviceName string, waitFlags commands.WaitFlags, verbDoing string, verbDone string, out io.Writer) error {
if !waitFlags.Wait {
func waitIfRequested(client clientservingv1.KnServingClient, waitFlags commands.WaitFlags, serviceName, verbDoing, verbDone, targetFlag string, out io.Writer) error {
if !waitFlags.Wait || targetFlag != "" {
fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", serviceName, verbDone, client.Namespace())
return nil
}
fmt.Fprintf(out, "%s service '%s' in namespace '%s':\n", verbDoing, serviceName, client.Namespace())
return waitForServiceToGetReady(client, serviceName, waitFlags.TimeoutInSeconds, verbDone, out)
}

View File

@ -41,7 +41,12 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
kn service delete svc2 -n ns1
# Delete all services in 'ns1' namespace
kn service delete --all -n ns1`,
kn service delete --all -n ns1
# Delete the services in offline mode instead of kubernetes cluster
kn service delete test -n test-ns --target=/user/knfiles
kn service delete test --target=/user/knfiles/test.yaml
kn service delete test --target=/user/knfiles/test.json`,
RunE: func(cmd *cobra.Command, args []string) error {
all, err := cmd.Flags().GetBool("all")
@ -62,7 +67,7 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
if err != nil {
return err
}
client, err := p.NewServingClient(namespace)
client, err := newServingClient(p, namespace, cmd.Flag("target").Value.String())
if err != nil {
return err
}
@ -100,6 +105,7 @@ func NewServiceDeleteCommand(p *commands.KnParams) *cobra.Command {
flags := serviceDeleteCommand.Flags()
flags.Bool("all", false, "Delete all services in a namespace.")
commands.AddNamespaceFlags(serviceDeleteCommand.Flags(), false)
commands.AddGitOpsFlags(serviceDeleteCommand.Flags())
waitFlags.AddConditionWaitFlags(serviceDeleteCommand, commands.WaitDefaultTimeout, "delete", "service", "deleted")
return serviceDeleteCommand
}

View File

@ -79,7 +79,12 @@ var describe_example = `
kn service describe svc -o yaml
# Print only service URL
kn service describe svc -o url`
kn service describe svc -o url
# Describe the services in offline mode instead of kubernetes cluster
kn service describe test -n test-ns --target=/user/knfiles
kn service describe test --target=/user/knfiles/test.yaml
kn service describe test --target=/user/knfiles/test.json`
// NewServiceDescribeCommand returns a new command for describing a service.
func NewServiceDescribeCommand(p *commands.KnParams) *cobra.Command {
@ -102,7 +107,7 @@ func NewServiceDescribeCommand(p *commands.KnParams) *cobra.Command {
return err
}
client, err := p.NewServingClient(namespace)
client, err := newServingClient(p, namespace, cmd.Flag("target").Value.String())
if err != nil {
return err
}
@ -141,6 +146,7 @@ func NewServiceDescribeCommand(p *commands.KnParams) *cobra.Command {
}
flags := command.Flags()
commands.AddNamespaceFlags(flags, false)
commands.AddGitOpsFlags(flags)
flags.BoolP("verbose", "v", false, "More output.")
machineReadablePrintFlags.AddFlags(command)
command.Flag("output").Usage = fmt.Sprintf("Output format. One of: %s.", strings.Join(append(machineReadablePrintFlags.AllowedFormats(), "url"), "|"))

View File

@ -123,7 +123,7 @@ func importWithOwnerRef(client clientservingv1.KnServingClient, filename string,
}
}
err = waitIfRequested(client, serviceName, waitFlags, "Importing", "imported", out)
err = waitIfRequested(client, waitFlags, serviceName, "Importing", "imported", "", out)
if err != nil {
return err
}

View File

@ -42,13 +42,20 @@ func NewServiceListCommand(p *commands.KnParams) *cobra.Command {
kn service list -o json
# List service 'web'
kn service list web`,
kn service list web
# List the services in offline mode instead of kubernetes cluster
kn service list --target=/user/knfiles
kn service list --target=/user/knfiles/test.json
kn service list --target=/user/knfiles/test.yaml
kn service list -n test-ns --target=/user/knfiles`,
RunE: func(cmd *cobra.Command, args []string) error {
namespace, err := p.GetNamespace(cmd)
if err != nil {
return err
}
client, err := p.NewServingClient(namespace)
client, err := newServingClient(p, namespace, cmd.Flag("target").Value.String())
if err != nil {
return err
}
@ -81,6 +88,7 @@ func NewServiceListCommand(p *commands.KnParams) *cobra.Command {
},
}
commands.AddNamespaceFlags(serviceListCommand.Flags(), true)
commands.AddGitOpsFlags(serviceListCommand.Flags())
serviceListFlags.AddFlags(serviceListCommand)
return serviceListCommand
}

View File

@ -74,3 +74,10 @@ func showUrl(client clientservingv1.KnServingClient, serviceName string, origina
return nil
}
func newServingClient(p *commands.KnParams, namespace, dir string) (clientservingv1.KnServingClient, error) {
if dir != "" {
return p.NewGitopsServingClient(namespace, dir)
}
return p.NewServingClient(namespace)
}

View File

@ -48,7 +48,12 @@ var updateExample = `
kn service update svc --untag testing --tag @latest=staging
# Add tag 'test' to echo-v3 revision with 10% traffic and rest to latest ready revision of service
kn service update svc --tag echo-v3=test --traffic test=10,@latest=90`
kn service update svc --tag echo-v3=test --traffic test=10,@latest=90
# Update the service in offline mode instead of kubernetes cluster
kn service update gitopstest -n test-ns --env KEY1=VALUE1 --target=/user/knfiles
kn service update gitopstest --env KEY1=VALUE1 --target=/user/knfiles/test.yaml
kn service update gitopstest --env KEY1=VALUE1 --target=/user/knfiles/test.json`
func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
var editFlags ConfigurationEditFlags
@ -67,8 +72,8 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
if err != nil {
return err
}
client, err := p.NewServingClient(namespace)
targetFlag := cmd.Flag("target").Value.String()
client, err := newServingClient(p, namespace, targetFlag)
if err != nil {
return err
}
@ -109,7 +114,7 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
}
out := cmd.OutOrStdout()
if waitFlags.Wait {
if waitFlags.Wait && targetFlag == "" {
fmt.Fprintf(out, "Updating Service '%s' in namespace '%s':\n", args[0], namespace)
fmt.Fprintln(out, "")
err := waitForService(client, name, out, waitFlags.TimeoutInSeconds)
@ -131,6 +136,7 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
}
commands.AddNamespaceFlags(serviceUpdateCommand.Flags(), false)
commands.AddGitOpsFlags(serviceUpdateCommand.Flags())
editFlags.AddUpdateFlags(serviceUpdateCommand)
waitFlags.AddConditionWaitFlags(serviceUpdateCommand, commands.WaitDefaultTimeout, "update", "service", "ready")
trafficFlags.Add(serviceUpdateCommand)

View File

@ -40,14 +40,15 @@ import (
// KnParams for creating commands. Useful for inserting mocks for testing.
type KnParams struct {
Output io.Writer
KubeCfgPath string
ClientConfig clientcmd.ClientConfig
NewServingClient func(namespace string) (clientservingv1.KnServingClient, error)
NewSourcesClient func(namespace string) (v1alpha2.KnSourcesClient, error)
NewEventingClient func(namespace string) (clienteventingv1beta1.KnEventingClient, error)
NewMessagingClient func(namespace string) (clientmessagingv1beta1.KnMessagingClient, error)
NewDynamicClient func(namespace string) (clientdynamic.KnDynamicClient, error)
Output io.Writer
KubeCfgPath string
ClientConfig clientcmd.ClientConfig
NewServingClient func(namespace string) (clientservingv1.KnServingClient, error)
NewGitopsServingClient func(namespace string, dir string) (clientservingv1.KnServingClient, error)
NewSourcesClient func(namespace string) (v1alpha2.KnSourcesClient, error)
NewEventingClient func(namespace string) (clienteventingv1beta1.KnEventingClient, error)
NewMessagingClient func(namespace string) (clientmessagingv1beta1.KnMessagingClient, error)
NewDynamicClient func(namespace string) (clientdynamic.KnDynamicClient, error)
// General global options
LogHTTP bool
@ -61,6 +62,10 @@ func (params *KnParams) Initialize() {
params.NewServingClient = params.newServingClient
}
if params.NewGitopsServingClient == nil {
params.NewGitopsServingClient = params.newGitopsServingClient
}
if params.NewSourcesClient == nil {
params.NewSourcesClient = params.newSourcesClient
}
@ -84,10 +89,17 @@ func (params *KnParams) newServingClient(namespace string) (clientservingv1.KnSe
return nil, err
}
client, _ := servingv1client.NewForConfig(restConfig)
client, err := servingv1client.NewForConfig(restConfig)
if err != nil {
return nil, err
}
return clientservingv1.NewKnServingClient(client, namespace), nil
}
func (params *KnParams) newGitopsServingClient(namespace string, dir string) (clientservingv1.KnServingClient, error) {
return clientservingv1.NewKnServingGitOpsClient(namespace, dir), nil
}
func (params *KnParams) newSourcesClient(namespace string) (v1alpha2.KnSourcesClient, error) {
restConfig, err := params.RestConfig()
if err != nil {

219
pkg/serving/v1/gitops.go Normal file
View File

@ -0,0 +1,219 @@
// Copyright 2020 The Knative Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/genericclioptions"
"knative.dev/client/pkg/wait"
servingv1 "knative.dev/serving/pkg/apis/serving/v1"
)
const (
ksvcKind = "ksvc"
)
// knServingGitOpsClient - kn service client
// to work on a local repo instead of a remote cluster
type knServingGitOpsClient struct {
dir string
namespace string
fileMode bool
fileFormat string
KnServingClient
}
// NewKnServingGitOpsClient returns an instance of the
// kn service gitops client
func NewKnServingGitOpsClient(namespace, dir string) KnServingClient {
mode, format := getFileModeAndType(dir)
return &knServingGitOpsClient{
dir: dir,
namespace: namespace,
fileMode: mode,
fileFormat: format,
}
}
func (cl *knServingGitOpsClient) getKsvcFilePath(name string) string {
if cl.fileMode {
return cl.dir
}
return filepath.Join(cl.dir, cl.namespace, ksvcKind, name+".yaml")
}
func getFileModeAndType(dir string) (bool, string) {
switch {
case strings.HasSuffix(dir, ".yaml"):
return true, "yaml"
case strings.HasSuffix(dir, ".yml"):
return true, "yaml"
case strings.HasSuffix(dir, ".json"):
return true, "json"
}
return false, "yaml"
}
// Namespace returns the namespace
func (cl *knServingGitOpsClient) Namespace() string {
return cl.namespace
}
// GetService returns the knative service for the name
func (cl *knServingGitOpsClient) GetService(name string) (*servingv1.Service, error) {
return readServiceFromFile(cl.getKsvcFilePath(name), name)
}
// ListServices lists the services in the path provided
func (cl *knServingGitOpsClient) ListServices(config ...ListConfig) (*servingv1.ServiceList, error) {
svcs, err := cl.listServicesFromDirectory()
if err != nil {
return nil, err
}
typeMeta := metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
}
serviceList := &servingv1.ServiceList{
TypeMeta: typeMeta,
Items: svcs,
}
return serviceList, nil
}
func (cl *knServingGitOpsClient) listServicesFromDirectory() ([]servingv1.Service, error) {
if cl.fileMode {
svc, err := readServiceFromFile(cl.dir, "")
if err != nil {
return nil, err
}
return []servingv1.Service{*svc}, nil
}
var services []servingv1.Service
root := cl.dir
if cl.namespace != "" {
root = filepath.Join(cl.dir, cl.namespace)
}
if _, err := os.Stat(root); err != nil {
return nil, err
}
if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
switch {
// skip if dir is not ksvc
case info.IsDir():
return nil
// skip non yaml files
case !strings.HasSuffix(info.Name(), ".yaml"):
return nil
// skip non ksvc dir
case !strings.Contains(path, ksvcKind):
return filepath.SkipDir
default:
svc, err := readServiceFromFile(path, "")
if err != nil {
return err
}
services = append(services, *svc)
return nil
}
}); err != nil {
return nil, err
}
return services, nil
}
// CreateService saves the knative service spec in
// yaml format in the local path provided
func (cl *knServingGitOpsClient) CreateService(service *servingv1.Service) error {
updateServingGvk(service)
if cl.fileMode {
return writeFile(service, cl.dir, cl.fileFormat)
}
//check if dir exist
if _, err := os.Stat(cl.dir); os.IsNotExist(err) {
return fmt.Errorf("directory '%s' not present, please create the directory and try again", cl.dir)
}
return writeFile(service, cl.getKsvcFilePath(service.ObjectMeta.Name), cl.fileFormat)
}
func writeFile(obj runtime.Object, fp, format string) error {
if _, err := os.Stat(fp); os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(fp), 0755)
}
w, err := os.Create(fp)
if err != nil {
return err
}
yamlPrinter, err := genericclioptions.NewJSONYamlPrintFlags().ToPrinter(format)
if err != nil {
return err
}
return yamlPrinter.PrintObj(obj, w)
}
// UpdateService updates the service in
// the local directory
func (cl *knServingGitOpsClient) UpdateService(service *servingv1.Service) error {
// check if file exist
if _, err := cl.GetService(service.ObjectMeta.Name); err != nil {
return err
}
// replace file
return cl.CreateService(service)
}
// UpdateServiceWithRetry updates the service in the local directory
func (cl *knServingGitOpsClient) UpdateServiceWithRetry(name string, updateFunc ServiceUpdateFunc, nrRetries int) error {
return updateServiceWithRetry(cl, name, updateFunc, nrRetries)
}
// DeleteService removes the file from the local file system
func (cl *knServingGitOpsClient) DeleteService(serviceName string, timeout time.Duration) error {
return os.Remove(cl.getKsvcFilePath(serviceName))
}
// WaitForService always returns success for this client
func (cl *knServingGitOpsClient) WaitForService(name string, timeout time.Duration, msgCallback wait.MessageCallback) (error, time.Duration) {
return nil, 1 * time.Second
}
func readServiceFromFile(fileKey, name string) (*servingv1.Service, error) {
var svc servingv1.Service
file, err := os.Open(fileKey)
if err != nil {
if os.IsNotExist(err) {
return nil, apierrors.NewNotFound(servingv1.Resource("services"), name)
}
return nil, err
}
decoder := yaml.NewYAMLOrJSONDecoder(file, 512)
if err := decoder.Decode(&svc); err != nil {
return nil, err
}
return &svc, nil
}

View File

@ -0,0 +1,253 @@
// Copyright 2020 The Knative Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
servingtest "knative.dev/serving/pkg/testing/v1"
libtest "knative.dev/client/lib/test"
"knative.dev/pkg/ptr"
servingv1 "knative.dev/serving/pkg/apis/serving/v1"
)
func TestGitOpsOperations(t *testing.T) {
c1TempDir, err := ioutil.TempDir("", "kn-files-cluster1")
assert.NilError(t, err)
c2TempDir, err := ioutil.TempDir("", "kn-files-cluster2")
assert.NilError(t, err)
defer os.RemoveAll(c1TempDir)
defer os.RemoveAll(c2TempDir)
// create clients
fooclient := NewKnServingGitOpsClient("foo-ns", c1TempDir)
bazclient := NewKnServingGitOpsClient("baz-ns", c1TempDir)
globalclient := NewKnServingGitOpsClient("", c1TempDir)
diffClusterClient := NewKnServingGitOpsClient("", "tmp")
// set up test services
fooSvc := libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()))
barSvc := libtest.BuildServiceWithOptions("bar", servingtest.WithConfigSpec(buildConfiguration()))
fooUpdateSvc := libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()), servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}))
fooserviceList := getServiceList([]servingv1.Service{*barSvc, *fooSvc})
allServices := getServiceList([]servingv1.Service{*barSvc, *barSvc, *fooSvc})
t.Run("get file path for foo service in foo namespace", func(t *testing.T) {
fp := fooclient.(*knServingGitOpsClient).getKsvcFilePath("foo")
assert.Equal(t, filepath.Join(c1TempDir, "foo-ns/ksvc/foo.yaml"), fp)
})
t.Run("get namespace for bazclient client", func(t *testing.T) {
ns := bazclient.Namespace()
assert.Equal(t, "baz-ns", ns)
})
t.Run("create service foo in foo namespace", func(t *testing.T) {
err := fooclient.CreateService(fooSvc)
assert.NilError(t, err)
})
t.Run("wait for foo service in foo namespace", func(t *testing.T) {
err, d := fooclient.WaitForService("foo", 5*time.Second, nil)
assert.NilError(t, err)
assert.Equal(t, 1*time.Second, d)
})
t.Run("get service foo", func(t *testing.T) {
result, err := fooclient.GetService("foo")
assert.NilError(t, err)
assert.DeepEqual(t, fooSvc, result)
})
t.Run("create service bar in foo namespace", func(t *testing.T) {
err := fooclient.CreateService(barSvc)
assert.NilError(t, err)
})
t.Run("create service bar in baz namespace", func(t *testing.T) {
err := bazclient.CreateService(barSvc)
assert.NilError(t, err)
})
t.Run("list services in foo namespace", func(t *testing.T) {
result, err := fooclient.ListServices()
assert.NilError(t, err)
assert.DeepEqual(t, fooserviceList, result)
})
t.Run("create service without master directory", func(t *testing.T) {
err := diffClusterClient.CreateService(fooSvc)
assert.ErrorContains(t, err, "directory 'tmp' not present, please create the directory and try again")
})
diffClusterClient = NewKnServingGitOpsClient("", c2TempDir)
t.Run("create service foo in foo namespace in cluster 2", func(t *testing.T) {
err := diffClusterClient.CreateService(fooSvc)
assert.NilError(t, err)
})
t.Run("list services in all namespaces in cluster 1", func(t *testing.T) {
result, err := globalclient.ListServices()
assert.NilError(t, err)
assert.DeepEqual(t, allServices, result)
})
t.Run("update service with retry foo", func(t *testing.T) {
err := fooclient.UpdateServiceWithRetry("foo", func(svc *servingv1.Service) (*servingv1.Service, error) {
return svc, nil
}, 1)
assert.NilError(t, err)
})
t.Run("update service foo", func(t *testing.T) {
err := fooclient.UpdateService(fooUpdateSvc)
assert.NilError(t, err)
})
t.Run("check updated service foo", func(t *testing.T) {
result, err := fooclient.GetService("foo")
assert.NilError(t, err)
assert.DeepEqual(t, fooUpdateSvc, result)
})
t.Run("delete service foo", func(t *testing.T) {
err := fooclient.DeleteService("foo", 5*time.Second)
assert.NilError(t, err)
})
t.Run("get service foo", func(t *testing.T) {
_, err := fooclient.GetService("foo")
assert.ErrorType(t, err, apierrors.IsNotFound)
})
}
func TestGitOpsSingleFile(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "singlefile")
assert.NilError(t, err)
defer os.RemoveAll(tmpDir)
// create clients
fooclient := NewKnServingGitOpsClient("", filepath.Join(tmpDir, "test.yaml"))
barclient := NewKnServingGitOpsClient("", filepath.Join(tmpDir, "test.yml"))
bazclient := NewKnServingGitOpsClient("", filepath.Join(tmpDir, "test.json"))
// set up test services
testSvc := libtest.BuildServiceWithOptions("test", servingtest.WithConfigSpec(buildConfiguration()))
updateSvc := libtest.BuildServiceWithOptions("test", servingtest.WithConfigSpec(buildConfiguration()), servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}))
svcList := getServiceList([]servingv1.Service{*updateSvc})
t.Run("get file path for fooclient", func(t *testing.T) {
fp := fooclient.(*knServingGitOpsClient).getKsvcFilePath("test")
assert.Equal(t, filepath.Join(tmpDir, "test.yaml"), fp)
})
t.Run("get namespace for fooclient", func(t *testing.T) {
ns := fooclient.Namespace()
assert.Equal(t, "", ns)
})
t.Run("create service in single file mode in different formats", func(t *testing.T) {
err := fooclient.CreateService(testSvc)
assert.NilError(t, err)
err = barclient.CreateService(testSvc)
assert.NilError(t, err)
err = bazclient.CreateService(testSvc)
assert.NilError(t, err)
})
t.Run("retrieve services", func(t *testing.T) {
result, err := fooclient.GetService("test")
assert.NilError(t, err)
assert.DeepEqual(t, testSvc, result)
result, err = barclient.GetService("test")
assert.NilError(t, err)
assert.DeepEqual(t, testSvc, result)
result, err = bazclient.GetService("test")
assert.NilError(t, err)
assert.DeepEqual(t, testSvc, result)
})
t.Run("update service foo", func(t *testing.T) {
err := fooclient.UpdateService(updateSvc)
assert.NilError(t, err)
err = barclient.UpdateService(updateSvc)
assert.NilError(t, err)
err = bazclient.UpdateService(updateSvc)
assert.NilError(t, err)
})
t.Run("list services", func(t *testing.T) {
result, err := fooclient.ListServices()
assert.NilError(t, err)
assert.DeepEqual(t, svcList, result)
result, err = barclient.ListServices()
assert.NilError(t, err)
assert.DeepEqual(t, svcList, result)
result, err = bazclient.ListServices()
assert.NilError(t, err)
assert.DeepEqual(t, svcList, result)
})
t.Run("delete service foo", func(t *testing.T) {
err := fooclient.DeleteService("test", 5*time.Second)
assert.NilError(t, err)
err = barclient.DeleteService("test", 5*time.Second)
assert.NilError(t, err)
err = bazclient.DeleteService("test", 5*time.Second)
assert.NilError(t, err)
})
t.Run("get service foo", func(t *testing.T) {
_, err := fooclient.GetService("test")
assert.ErrorType(t, err, apierrors.IsNotFound)
_, err = barclient.GetService("test")
assert.ErrorType(t, err, apierrors.IsNotFound)
_, err = bazclient.GetService("test")
assert.ErrorType(t, err, apierrors.IsNotFound)
})
}
func getServiceList(services []servingv1.Service) *servingv1.ServiceList {
return &servingv1.ServiceList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
},
Items: services,
}
}
func buildConfiguration() *servingv1.ConfigurationSpec {
c := &servingv1.Configuration{
Spec: servingv1.ConfigurationSpec{
Template: servingv1.RevisionTemplateSpec{
Spec: *revisionSpec.DeepCopy(),
},
},
}
c.SetDefaults(context.Background())
return &c.Spec
}
var revisionSpec = servingv1.RevisionSpec{
PodSpec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "busybox",
}},
EnableServiceLinks: ptr.Bool(false),
},
TimeoutSeconds: ptr.Int64(300),
}