Merge "Add poller"

This commit is contained in:
Zuul 2020-08-20 22:04:23 +00:00 committed by Gerrit Code Review
commit d3ff9bc814
8 changed files with 306 additions and 27 deletions

1
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.4.0
golang.org/x/tools v0.0.0-20200619210111-0f592d2728bb // indirect
k8s.io/api v0.17.4
k8s.io/apiextensions-apiserver v0.17.4
k8s.io/apimachinery v0.17.4

2
go.sum
View File

@ -1300,6 +1300,8 @@ golang.org/x/tools v0.0.0-20200327195553-82bb89366a1e/go.mod h1:Sl4aGygMT6LrqrWc
golang.org/x/tools v0.0.0-20200331202046-9d5940d49312/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk=
golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619210111-0f592d2728bb h1:/7SQoPdMxZ0c/Zu9tBJgMbRE/BmK6i9QXflNJXKAmw0=
golang.org/x/tools v0.0.0-20200619210111-0f592d2728bb/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=

View File

@ -15,32 +15,31 @@
package cluster
import (
"context"
"encoding/json"
"fmt"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/k8s/client"
)
// A Status represents a kubernetes resource's state.
type Status string
// These represent the default statuses
const (
UnknownStatus = Status("Unknown")
)
// StatusMap holds a mapping of schema.GroupVersionResource to various statuses
// a resource may be in, as well as the Expression used to check for that
// status.
type StatusMap struct {
client client.Interface
mapping map[schema.GroupVersionResource]map[Status]Expression
GkMapping []schema.GroupKind
mapping map[schema.GroupVersionResource]map[status.Status]Expression
restMapper *meta.DefaultRESTMapper
}
@ -52,10 +51,10 @@ type StatusMap struct {
func NewStatusMap(client client.Interface) (*StatusMap, error) {
statusMap := &StatusMap{
client: client,
mapping: make(map[schema.GroupVersionResource]map[Status]Expression),
mapping: make(map[schema.GroupVersionResource]map[status.Status]Expression),
restMapper: meta.NewDefaultRESTMapper([]schema.GroupVersion{}),
}
client.ApiextensionsClientSet()
crds, err := statusMap.client.ApiextensionsClientSet().
ApiextensionsV1().
CustomResourceDefinitions().
@ -73,9 +72,84 @@ func NewStatusMap(client client.Interface) (*StatusMap, error) {
return statusMap, nil
}
// ReadStatus returns object status
func (sm *StatusMap) ReadStatus(ctx context.Context, resource object.ObjMetadata) *event.ResourceStatus {
gk := resource.GroupKind
gvr, err := sm.restMapper.RESTMapping(gk, "v1")
if err != nil {
return handleResourceStatusError(resource, err)
}
options := metav1.GetOptions{}
object, err := sm.client.DynamicClient().Resource(gvr.Resource).
Namespace(resource.Namespace).Get(resource.Name, options)
if err != nil {
return handleResourceStatusError(resource, err)
}
return sm.ReadStatusForObject(ctx, object)
}
// ReadStatusForObject returns resource status for object.
// Current status will be returned only if expression matched.
func (sm *StatusMap) ReadStatusForObject(
ctx context.Context, resource *unstructured.Unstructured) *event.ResourceStatus {
identifier := object.ObjMetadata{
GroupKind: resource.GroupVersionKind().GroupKind(),
Name: resource.GetName(),
Namespace: resource.GetNamespace(),
}
gvk := resource.GroupVersionKind()
restMapping, err := sm.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return &event.ResourceStatus{
Identifier: identifier,
Status: status.UnknownStatus,
Error: err,
}
}
gvr := restMapping.Resource
obj, err := sm.client.DynamicClient().Resource(gvr).Namespace(resource.GetNamespace()).
Get(resource.GetName(), metav1.GetOptions{})
if err != nil {
return &event.ResourceStatus{
Identifier: identifier,
Status: status.UnknownStatus,
Error: err,
}
}
// No need to check for existence - if there isn't a mapping for this
// resource, the following for loop won't run anyway
for currentstatus, expression := range sm.mapping[gvr] {
var matched bool
matched, err = expression.Match(obj)
if err != nil {
return &event.ResourceStatus{
Identifier: identifier,
Status: status.UnknownStatus,
Error: err,
}
}
if matched {
return &event.ResourceStatus{
Identifier: identifier,
Status: currentstatus,
Resource: resource,
Message: fmt.Sprintf("%s is %s", resource.GroupVersionKind().Kind, currentstatus.String()),
}
}
}
return &event.ResourceStatus{
Identifier: identifier,
Status: status.UnknownStatus,
Error: nil,
}
}
// GetStatusForResource iterates over all of the stored conditions for the
// resource and returns the first status whose conditions are met.
func (sm *StatusMap) GetStatusForResource(resource document.Document) (Status, error) {
func (sm *StatusMap) GetStatusForResource(resource document.Document) (status.Status, error) {
gvk := getGVK(resource)
restMapping, err := sm.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
@ -103,7 +177,7 @@ func (sm *StatusMap) GetStatusForResource(resource document.Document) (Status, e
}
}
return UnknownStatus, nil
return status.UnknownStatus, nil
}
// addCRD adds the mappings from the CRD to its associated statuses
@ -122,6 +196,7 @@ func (sm *StatusMap) addCRD(crd apiextensions.CustomResourceDefinition) error {
gvrs := getGVRs(crd)
for _, gvr := range gvrs {
sm.GkMapping = append(sm.GkMapping, crd.GroupVersionKind().GroupKind())
gvk := gvr.GroupVersion().WithKind(crd.Spec.Names.Kind)
gvrSingular := gvr.GroupVersion().WithResource(crd.Spec.Names.Singular)
sm.mapping[gvr] = statusChecks
@ -159,7 +234,7 @@ func getGVK(doc document.Document) schema.GroupVersionKind {
// parseStatusChecks takes a string containing a map of status names (e.g.
// Healthy) to the JSONPath filters associated with the statuses, and returns
// the Go object equivalent.
func parseStatusChecks(raw string) (map[Status]Expression, error) {
func parseStatusChecks(raw string) (map[status.Status]Expression, error) {
type statusCheckType struct {
Status string `json:"status"`
Condition string `json:"condition"`
@ -172,7 +247,7 @@ func parseStatusChecks(raw string) (map[Status]Expression, error) {
}
}
expressionMap := make(map[Status]Expression)
expressionMap := make(map[status.Status]Expression)
for _, mapping := range mappings {
if mapping.Status == "" {
return nil, ErrInvalidStatusCheck{What: "missing status field"}
@ -182,8 +257,25 @@ func parseStatusChecks(raw string) (map[Status]Expression, error) {
return nil, ErrInvalidStatusCheck{What: "missing condition field"}
}
expressionMap[Status(mapping.Status)] = Expression{Condition: mapping.Condition}
expressionMap[status.Status(mapping.Status)] = Expression{Condition: mapping.Condition}
}
return expressionMap, nil
}
// handleResourceStatusError construct the appropriate ResourceStatus
// object based on the type of error.
func handleResourceStatusError(identifier object.ObjMetadata, err error) *event.ResourceStatus {
if errors.IsNotFound(err) {
return &event.ResourceStatus{
Identifier: identifier,
Status: status.NotFoundStatus,
Message: "Resource not found",
}
}
return &event.ResourceStatus{
Identifier: identifier,
Status: status.UnknownStatus,
Error: err,
}
}

View File

@ -15,14 +15,17 @@
package cluster_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"opendev.org/airship/airshipctl/pkg/cluster"
"opendev.org/airship/airshipctl/pkg/document"
@ -76,7 +79,7 @@ func TestGetStatusForResource(t *testing.T) {
name string
selector document.Selector
client *fake.Client
expectedStatus cluster.Status
expectedStatus status.Status
err error
}{
{
@ -86,9 +89,9 @@ func TestGetStatusForResource(t *testing.T) {
ByName("stable-resource"),
client: fake.NewClient(
fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("Resource", "stable-resource", "stable")),
fake.WithDynamicObjects(makeResource("stable-resource", "stable")),
),
expectedStatus: cluster.Status("Stable"),
expectedStatus: status.Status("Stable"),
},
{
name: "pending-resource-is-pending",
@ -97,9 +100,9 @@ func TestGetStatusForResource(t *testing.T) {
ByName("pending-resource"),
client: fake.NewClient(
fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("Resource", "pending-resource", "pending")),
fake.WithDynamicObjects(makeResource("pending-resource", "pending")),
),
expectedStatus: cluster.Status("Pending"),
expectedStatus: status.Status("Pending"),
},
{
name: "unknown-resource-is-unknown",
@ -108,9 +111,9 @@ func TestGetStatusForResource(t *testing.T) {
ByName("unknown"),
client: fake.NewClient(
fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("Resource", "unknown", "unknown")),
fake.WithDynamicObjects(makeResource("unknown", "unknown")),
),
expectedStatus: cluster.UnknownStatus,
expectedStatus: status.UnknownStatus,
},
{
name: "missing-resource-returns-error",
@ -146,11 +149,23 @@ func TestGetStatusForResource(t *testing.T) {
}
}
func makeResource(kind, name, state string) *unstructured.Unstructured {
func TestReadStatus(t *testing.T) {
c := fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("pending-resource", "pending")))
statusMap, err := cluster.NewStatusMap(c)
require.NoError(t, err)
ctx := context.Background()
resource := object.ObjMetadata{Namespace: "default",
Name: "pending-resource", GroupKind: schema.GroupKind{Group: "example.com", Kind: "Resource"}}
result := statusMap.ReadStatus(ctx, resource)
assert.Equal(t, "Pending", result.Status.String())
}
func makeResource(name, state string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "example.com/v1",
"kind": kind,
"kind": "Resource",
"metadata": map[string]interface{}{
"name": name,
"namespace": "default",

99
pkg/k8s/poller/poller.go Normal file
View File

@ -0,0 +1,99 @@
/*
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
https://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 poller
import (
"context"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/statusreaders"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/controller-runtime/pkg/client"
"opendev.org/airship/airshipctl/pkg/cluster"
)
// NewStatusPoller creates a new StatusPoller using the given clusterreader and mapper. The StatusPoller
// will use the client for all calls to the cluster.
func NewStatusPoller(reader client.Reader, mapper meta.RESTMapper, statusmap *cluster.StatusMap) *StatusPoller {
return &StatusPoller{
engine: &engine.PollerEngine{
Reader: reader,
Mapper: mapper,
},
statusmap: statusmap,
}
}
// StatusPoller provides functionality for polling a cluster for status for a set of resources.
type StatusPoller struct {
engine *engine.PollerEngine
statusmap *cluster.StatusMap
}
// Poll will create a new statusPollerRunner that will poll all the resources provided and report their status
// back on the event channel returned. The statusPollerRunner can be canceled at any time by canceling the
// context passed in.
func (s *StatusPoller) Poll(
ctx context.Context, identifiers []object.ObjMetadata, options polling.Options) <-chan event.Event {
return s.engine.Poll(ctx, identifiers, engine.Options{
PollInterval: options.PollInterval,
ClusterReaderFactoryFunc: clusterReaderFactoryFunc(options.UseCache),
StatusReadersFactoryFunc: s.createStatusReaders,
})
}
// createStatusReaders creates an instance of all the statusreaders. This includes a set of statusreaders for
// a particular GroupKind, and a default engine used for all resource types that does not have
// a specific statusreaders.
// TODO: We should consider making the registration more automatic instead of having to create each of them
// here. Also, it might be worth creating them on demand.
func (s *StatusPoller) createStatusReaders(reader engine.ClusterReader, mapper meta.RESTMapper) (
map[schema.GroupKind]engine.StatusReader, engine.StatusReader) {
defaultStatusReader := statusreaders.NewGenericStatusReader(reader, mapper)
replicaSetStatusReader := statusreaders.NewReplicaSetStatusReader(reader, mapper, defaultStatusReader)
deploymentStatusReader := statusreaders.NewDeploymentResourceReader(reader, mapper, replicaSetStatusReader)
statefulSetStatusReader := statusreaders.NewStatefulSetResourceReader(reader, mapper, defaultStatusReader)
statusReaders := map[schema.GroupKind]engine.StatusReader{
appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind(): deploymentStatusReader,
appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind(): statefulSetStatusReader,
appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind(): replicaSetStatusReader,
}
for _, gk := range s.statusmap.GkMapping {
statusReaders[gk] = s.statusmap
}
return statusReaders, defaultStatusReader
}
// clusterReaderFactoryFunc returns a factory function for creating an instance of a ClusterReader.
// This function is used by the StatusPoller to create a ClusterReader for each StatusPollerRunner.
// The decision for which implementation of the ClusterReader interface that should be used are
// decided here rather than based on information passed in to the factory function. Thus, the decision
// for which implementation is decided when the StatusPoller is created.
func clusterReaderFactoryFunc(useCache bool) engine.ClusterReaderFactoryFunc {
return func(r client.Reader, mapper meta.RESTMapper, identifiers []object.ObjMetadata) (engine.ClusterReader, error) {
if useCache {
return clusterreader.NewCachingClusterReader(r, mapper, identifiers)
}
return &clusterreader.DirectClusterReader{Reader: r}, nil
}
}

52
pkg/k8s/poller/poller_test.go Executable file
View File

@ -0,0 +1,52 @@
/*
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
https://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 poller_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/controller-runtime/pkg/client"
"opendev.org/airship/airshipctl/pkg/cluster"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/pkg/k8s/poller"
k8sutils "opendev.org/airship/airshipctl/pkg/k8s/utils"
)
func TestNewStatusPoller(t *testing.T) {
settings := &environment.AirshipCTLSettings{
Debug: true,
Config: config.NewConfig(),
KubeConfigPath: "testdata/kubeconfig.yaml",
}
airClient := fake.NewClient()
f := k8sutils.FactoryFromKubeConfigPath(settings.KubeConfigPath)
restConfig, err := f.ToRESTConfig()
require.NoError(t, err)
restMapper, err := f.ToRESTMapper()
require.NoError(t, err)
restClient, err := client.New(restConfig, client.Options{Mapper: restMapper})
require.NoError(t, err)
statusmap, err := cluster.NewStatusMap(airClient)
require.NoError(t, err)
a := poller.NewStatusPoller(restClient, restMapper, statusmap)
assert.NotNil(t, a)
}

19
pkg/k8s/poller/testdata/kubeconfig.yaml vendored Executable file
View File

@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1Ea3lPVEUzTURNd09Wb1hEVEk1TURreU5qRTNNRE13T1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUZyCkdxM0kyb2dZci81Y01Udy9Na1pORTNWQURzdEdyU240WjU2TDhPUGhMcUhDN2t1dno2dVpES3dCSGtGeTBNK2MKRXIzd2piUGE1aTV5NmkyMGtxSHBVMjdPZTA0dzBXV2s4N0RSZVlWaGNoZVJHRXoraWt3SndIcGRmMjJVemZNKwpkSDBzaUhuMVd6UnovYk4za3hMUzJlMnZ2U1Y3bmNubk1YRUd4OXV0MUY0NThHeWxxdmxXTUlWMzg5Q2didXFDCkcwcFdiMTBLM0RVZWdiT25Xa1FmSm5sTWRRVVZDUVdZZEZaaklrcWtkWi9hVTRobkNEV01oZXNWRnFNaDN3VVAKczhQay9BNWh1ZFFPbnFRNDVIWXZLdjZ5RjJWcDUyWExBRUx3NDJ4aVRKZlh0V1h4eHR6cU4wY1lyL2VxeS9XMQp1YVVGSW5xQjFVM0JFL1oxbmFrQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKUUVKQVBLSkFjVDVuK3dsWGJsdU9mS0J3c2gKZTI4R1c5R2QwM0N0NGF3RzhzMXE1ZHNua2tpZmVTUENHVFZ1SXF6UTZDNmJaSk9SMDMvVEl5ejh6NDJnaitDVApjWUZXZkltM2RKTnpRL08xWkdySXZZNWdtcWJtWDlpV0JaU24rRytEOGxubzd2aGMvY0tBRFR5OTMvVU92MThuCkdhMnIrRGJJcHcyTWVBVEl2elpxRS9RWlVSQ25DMmdjUFhTVzFqN2h4R3o1a3ZNcGVDZTdQYVUvdVFvblVHSWsKZ2t6ZzI4NHQvREhUUzc4N1V1SUg5cXBaV09yTFNMOGFBeUxQUHhWSXBteGZmbWRETE9TS2VUemRlTmxoSitUMwowQlBVaHBQTlJBNTNJN0hRQjhVUDR2elNONTkzZ1VFbVlFQ2Jic2RYSzB6ZVR6SDdWWHR2Zmd5WTVWWT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://10.0.1.7:6443
name: kubernetes_target
contexts:
- context:
cluster: kubernetes_target
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: ""
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQXhEdzk2RUY4SXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBNU1qa3hOekF6TURsYUZ3MHlNREE1TWpneE56QXpNVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV6R0pZdlBaNkRvaTQyMUQKSzhXSmFaQ25OQWQycXo1cC8wNDJvRnpRUGJyQWd6RTJxWVZrek9MOHhBVmVSN1NONXdXb1RXRXlGOEVWN3JyLwo0K0hoSEdpcTVQbXF1SUZ5enpuNi9JWmM4alU5eEVmenZpa2NpckxmVTR2UlhKUXdWd2dBU05sMkFXQUloMmRECmRUcmpCQ2ZpS1dNSHlqMFJiSGFsc0J6T3BnVC9IVHYzR1F6blVRekZLdjJkajVWMU5rUy9ESGp5UlJKK0VMNlEKQlltR3NlZzVQNE5iQzllYnVpcG1NVEFxL0p1bU9vb2QrRmpMMm5acUw2Zkk2ZkJ0RjVPR2xwQ0IxWUo4ZnpDdApHUVFaN0hUSWJkYjJ0cDQzRlZPaHlRYlZjSHFUQTA0UEoxNSswV0F5bVVKVXo4WEE1NDRyL2J2NzRKY0pVUkZoCmFyWmlRd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMMmhIUmVibEl2VHJTMFNmUVg1RG9ueVVhNy84aTg1endVWApSd3dqdzFuS0U0NDJKbWZWRGZ5b0hRYUM4Ti9MQkxyUXM0U0lqU1JYdmFHU1dSQnRnT1RRV21Db1laMXdSbjdwCndDTXZQTERJdHNWWm90SEZpUFl2b1lHWFFUSXA3YlROMmg1OEJaaEZ3d25nWUovT04zeG1rd29IN1IxYmVxWEYKWHF1TTluekhESk41VlZub1lQR09yRHMwWlg1RnNxNGtWVU0wVExNQm9qN1ZIRDhmU0E5RjRYNU4yMldsZnNPMAo4aksrRFJDWTAyaHBrYTZQQ0pQS0lNOEJaMUFSMG9ZakZxT0plcXpPTjBqcnpYWHh4S2pHVFVUb1BldVA5dCtCCjJOMVA1TnI4a2oxM0lrend5Q1NZclFVN09ZM3ltZmJobHkrcXZxaFVFa014MlQ1SkpmQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdXpHSll2UFo2RG9pNDIxREs4V0phWkNuTkFkMnF6NXAvMDQyb0Z6UVBickFnekUyCnFZVmt6T0w4eEFWZVI3U041d1dvVFdFeUY4RVY3cnIvNCtIaEhHaXE1UG1xdUlGeXp6bjYvSVpjOGpVOXhFZnoKdmlrY2lyTGZVNHZSWEpRd1Z3Z0FTTmwyQVdBSWgyZERkVHJqQkNmaUtXTUh5ajBSYkhhbHNCek9wZ1QvSFR2MwpHUXpuVVF6Rkt2MmRqNVYxTmtTL0RIanlSUkorRUw2UUJZbUdzZWc1UDROYkM5ZWJ1aXBtTVRBcS9KdW1Pb29kCitGakwyblpxTDZmSTZmQnRGNU9HbHBDQjFZSjhmekN0R1FRWjdIVEliZGIydHA0M0ZWT2h5UWJWY0hxVEEwNFAKSjE1KzBXQXltVUpVejhYQTU0NHIvYnY3NEpjSlVSRmhhclppUXdJREFRQUJBb0lCQVFDU0pycjlaeVpiQ2dqegpSL3VKMFZEWCt2aVF4c01BTUZyUjJsOE1GV3NBeHk1SFA4Vk4xYmc5djN0YUVGYnI1U3hsa3lVMFJRNjNQU25DCm1uM3ZqZ3dVQWlScllnTEl5MGk0UXF5VFBOU1V4cnpTNHRxTFBjM3EvSDBnM2FrNGZ2cSsrS0JBUUlqQnloamUKbnVFc1JpMjRzT3NESlM2UDE5NGlzUC9yNEpIM1M5bFZGbkVuOGxUR2c0M1kvMFZoMXl0cnkvdDljWjR5ZUNpNwpjMHFEaTZZcXJZaFZhSW9RRW1VQjdsbHRFZkZzb3l4VDR6RTE5U3pVbkRoMmxjYTF1TzhqcmI4d2xHTzBoQ2JyClB1R1l2WFFQa3Q0VlNmalhvdGJ3d2lBNFRCVERCRzU1bHp6MmNKeS9zSS8zSHlYbEMxcTdXUmRuQVhhZ1F0VzkKOE9DZGRkb0JBb0dCQU5NcUNtSW94REtyckhZZFRxT1M1ZFN4cVMxL0NUN3ZYZ0pScXBqd2Y4WHA2WHo0KzIvTAozVXFaVDBEL3dGTkZkc1Z4eFYxMnNYMUdwMHFWZVlKRld5OVlCaHVSWGpTZ0ZEWldSY1Z1Y01sNVpPTmJsbmZGCjVKQ0xnNXFMZ1g5VTNSRnJrR3A0R241UDQxamg4TnhKVlhzZG5xWE9xNTFUK1RRT1UzdkpGQjc1QW9HQkFPTHcKalp1cnZtVkZyTHdaVGgvRDNpWll5SVV0ZUljZ2NKLzlzbTh6L0pPRmRIbFd4dGRHUFVzYVd1MnBTNEhvckFtbgpqTm4vSTluUXd3enZ3MWUzVVFPbUhMRjVBczk4VU5hbk5TQ0xNMW1yaXZHRXJ1VHFnTDM1bU41eFZPdTUxQU5JCm4yNkFtODBJT2JDeEtLa0R0ZXJSaFhHd3g5c1pONVJCbG9VRThZNGJBb0dBQ3ZsdVhMZWRxcng5VkE0bDNoNXUKVDJXRVUxYjgxZ1orcmtRc1I1S0lNWEw4cllBTElUNUpHKzFuendyN3BkaEFXZmFWdVV2SDRhamdYT0h6MUs5aQpFODNSVTNGMG9ldUg0V01PY1RwU0prWm0xZUlXcWRiaEVCb1FGdUlWTXRib1BsV0d4ZUhFRHJoOEtreGp4aThSCmdEcUQyajRwY1IzQ0g5QjJ5a0lqQjVFQ2dZRUExc0xXLys2enE1c1lNSm14K1JXZThhTXJmL3pjQnVTSU1LQWgKY0dNK0wwMG9RSHdDaUU4TVNqcVN1ajV3R214YUFuanhMb3ZwSFlRV1VmUEVaUW95UE1YQ2VhRVBLOU4xbk8xMwp0V2lHRytIZkIxaU5PazFCc0lhNFNDbndOM1FRVTFzeXBaeEgxT3hueS9LYmkvYmEvWEZ5VzNqMGFUK2YvVWxrCmJGV1ZVdWtDZ1lFQTBaMmRTTFlmTjV5eFNtYk5xMWVqZXdWd1BjRzQxR2hQclNUZEJxdHFac1doWGE3aDdLTWEKeHdvamh5SXpnTXNyK2tXODdlajhDQ2h0d21sQ1p5QU92QmdOZytncnJ1cEZLM3FOSkpKeU9YREdHckdpbzZmTQp5aXB3Q2tZVGVxRThpZ1J6UkI5QkdFUGY4eVpjMUtwdmZhUDVhM0lRZmxiV0czbGpUemNNZVZjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=

View File

@ -117,7 +117,6 @@ func TestDeploy(t *testing.T) {
// makeNewFakeRootSettings takes kubeconfig path and directory path to fixture dir as argument.
func makeNewFakeRootSettings(t *testing.T, kp string, dir string) *environment.AirshipCTLSettings {
t.Helper()
akp, err := filepath.Abs(kp)
require.NoError(t, err)