From 5e04d71be507910d7e66bc5a8360d066abac6788 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 9 Dec 2020 20:35:09 -0500 Subject: [PATCH 01/15] Start building out shim layer for Tinkerbell Signed-off-by: Jason DeTiberus --- Makefile | 2 + api/v1alpha3/doc.go | 14 + config/crd/bases/tinkerbell.org_hardware.yaml | 50 +++ .../crd/bases/tinkerbell.org_templates.yaml | 53 ++++ .../crd/bases/tinkerbell.org_workflows.yaml | 64 ++++ go.sum | 2 - tink/api/v1alpha1/doc.go | 14 + tink/api/v1alpha1/groupversion_info.go | 33 ++ tink/api/v1alpha1/hardware_types.go | 60 ++++ tink/api/v1alpha1/template_types.go | 65 ++++ tink/api/v1alpha1/workflow_types.go | 70 +++++ tink/api/v1alpha1/zz_generated.deepcopy.go | 292 ++++++++++++++++++ tink/controllers/template_controller.go | 104 +++++++ 13 files changed, 821 insertions(+), 2 deletions(-) create mode 100644 api/v1alpha3/doc.go create mode 100644 config/crd/bases/tinkerbell.org_hardware.yaml create mode 100644 config/crd/bases/tinkerbell.org_templates.yaml create mode 100644 config/crd/bases/tinkerbell.org_workflows.yaml create mode 100644 tink/api/v1alpha1/doc.go create mode 100644 tink/api/v1alpha1/groupversion_info.go create mode 100644 tink/api/v1alpha1/hardware_types.go create mode 100644 tink/api/v1alpha1/template_types.go create mode 100644 tink/api/v1alpha1/workflow_types.go create mode 100644 tink/api/v1alpha1/zz_generated.deepcopy.go create mode 100644 tink/controllers/template_controller.go diff --git a/Makefile b/Makefile index 8704c8b5..a3476aed 100644 --- a/Makefile +++ b/Makefile @@ -293,6 +293,8 @@ generate-manifests: $(CONTROLLER_GEN) # Generate manifests e.g. CRD, RBAC etc. $(CONTROLLER_GEN) \ paths=./api/... \ paths=./controllers/... \ + paths=./tink/api/... \ + paths=./tink/controllers/... \ crd:crdVersions=v1 \ rbac:roleName=manager-role \ output:crd:dir=./config/crd/bases \ diff --git a/api/v1alpha3/doc.go b/api/v1alpha3/doc.go new file mode 100644 index 00000000..8f663577 --- /dev/null +++ b/api/v1alpha3/doc.go @@ -0,0 +1,14 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha3 diff --git a/config/crd/bases/tinkerbell.org_hardware.yaml b/config/crd/bases/tinkerbell.org_hardware.yaml new file mode 100644 index 00000000..a32def9d --- /dev/null +++ b/config/crd/bases/tinkerbell.org_hardware.yaml @@ -0,0 +1,50 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: hardware.tinkerbell.org +spec: + group: tinkerbell.org + names: + categories: + - tinkerbell + kind: Hardware + listKind: HardwareList + plural: hardware + singular: hardware + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Hardware is the Schema for the Hardware API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: HardwareSpec defines the desired state of Hardware. + type: object + status: + description: HardwareStatus defines the observed state of Hardware. + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/tinkerbell.org_templates.yaml b/config/crd/bases/tinkerbell.org_templates.yaml new file mode 100644 index 00000000..8968bfdc --- /dev/null +++ b/config/crd/bases/tinkerbell.org_templates.yaml @@ -0,0 +1,53 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: templates.tinkerbell.org +spec: + group: tinkerbell.org + names: + categories: + - tinkerbell + kind: Template + listKind: TemplateList + plural: templates + singular: template + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Template is the Schema for the Templates API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TemplateSpec defines the desired state of Template. + properties: + data: + type: string + type: object + status: + description: TemplateStatus defines the observed state of Template. + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/tinkerbell.org_workflows.yaml b/config/crd/bases/tinkerbell.org_workflows.yaml new file mode 100644 index 00000000..68fc98e3 --- /dev/null +++ b/config/crd/bases/tinkerbell.org_workflows.yaml @@ -0,0 +1,64 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: workflows.tinkerbell.org +spec: + group: tinkerbell.org + names: + categories: + - tinkerbell + kind: Workflow + listKind: WorkflowList + plural: workflows + singular: workflow + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Workflow is the Schema for the Workflows API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: WorkflowSpec defines the desired state of Workflow. + properties: + hardware: + description: Name of the Hardware associated with this workflow. + type: string + template: + description: Name of the Template associated with this Workflow. + type: string + type: object + status: + description: WorkflowStatus defines the observed state of Workflow. + properties: + data: + description: Data is the populated Workflow Data in Tinkerbell. + type: string + state: + description: State is the state of the workflow in Tinkerbell. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/go.sum b/go.sum index fe8a0955..3f344251 100644 --- a/go.sum +++ b/go.sum @@ -589,7 +589,6 @@ k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKf k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29 h1:NeQXVJ2XFSkRoPzRo8AId01ZER+j8oV4SZADT4iBOXQ= k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29/go.mod h1:F+5wygcW0wmRTnM3cOgIqGivxkwSWIWT5YdsDbeAOaU= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 h1:7Nu2dTj82c6IaWvL7hImJzcXoTPz1MsSCH7r+0m6rfo= k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -603,7 +602,6 @@ sigs.k8s.io/cluster-api v0.3.12/go.mod h1:KuUGKyD9+8TwYIcwoqDaScWKsee74/bpPo2sqQ sigs.k8s.io/controller-runtime v0.5.14 h1:lmoRaPvLg9877ZOnjFivjtyIdqyLbWfcCEilxHXTEj4= sigs.k8s.io/controller-runtime v0.5.14/go.mod h1:OTqxLuz7gVcrq+BHGUgedRu6b2VIKCEc7Pu4Jbwui0A= sigs.k8s.io/kind v0.7.1-0.20200303021537-981bd80d3802/go.mod h1:HIZ3PWUezpklcjkqpFbnYOqaqsAE1JeCTEwkgvPLXjk= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/tink/api/v1alpha1/doc.go b/tink/api/v1alpha1/doc.go new file mode 100644 index 00000000..5abc18bf --- /dev/null +++ b/tink/api/v1alpha1/doc.go @@ -0,0 +1,14 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 diff --git a/tink/api/v1alpha1/groupversion_info.go b/tink/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..729cf09b --- /dev/null +++ b/tink/api/v1alpha1/groupversion_info.go @@ -0,0 +1,33 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 contains API Schema definitions for the Tinkerbell v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=tinkerbell.org +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "tinkerbell.org", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/tink/api/v1alpha1/hardware_types.go b/tink/api/v1alpha1/hardware_types.go new file mode 100644 index 00000000..1596479c --- /dev/null +++ b/tink/api/v1alpha1/hardware_types.go @@ -0,0 +1,60 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HardwareIDAnnotation is used by the controller to store the +// ID assigned to the workflow by Tinkerbell. +const HardwareIDAnnotation = "hardware.tinkerbell.org/id" + +// HardwareSpec defines the desired state of Hardware. +type HardwareSpec struct { +} + +// HardwareStatus defines the observed state of Hardware. +type HardwareStatus struct { +} + +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=hardware,scope=Cluster,categories=tinkerbell +// +kubebuilder:storageversion + +// Hardware is the Schema for the Hardware API. +type Hardware struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HardwareSpec `json:"spec,omitempty"` + Status HardwareStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// HardwareList contains a list of Hardware. +type HardwareList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Hardware `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Hardware{}, &HardwareList{}) +} diff --git a/tink/api/v1alpha1/template_types.go b/tink/api/v1alpha1/template_types.go new file mode 100644 index 00000000..e9104830 --- /dev/null +++ b/tink/api/v1alpha1/template_types.go @@ -0,0 +1,65 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // TemplateIDAnnotation is used by the controller to store the + // ID assigned to the workflow by Tinkerbell. + TemplateIDAnnotation = "template.tinkerbell.org/id" + + TemplateFinalizer = "template.tinkerbell.org" +) + +// TemplateSpec defines the desired state of Template. +type TemplateSpec struct { + Data string `json:"data,omitempty"` +} + +// TemplateStatus defines the observed state of Template. +type TemplateStatus struct { +} + +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=templates,scope=Cluster,categories=tinkerbell +// +kubebuilder:storageversion + +// Template is the Schema for the Templates API. +type Template struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TemplateSpec `json:"spec,omitempty"` + Status TemplateStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// TemplateList contains a list of Templates. +type TemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Template `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Template{}, &TemplateList{}) +} diff --git a/tink/api/v1alpha1/workflow_types.go b/tink/api/v1alpha1/workflow_types.go new file mode 100644 index 00000000..7822f816 --- /dev/null +++ b/tink/api/v1alpha1/workflow_types.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Kubernetes 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// WorkflowIDAnnotation is used by the controller to store the +// ID assigned to the workflow by Tinkerbell. +const WorkflowIDAnnotation = "workflow.tinkerbell.org/id" + +// WorkflowSpec defines the desired state of Workflow. +type WorkflowSpec struct { + // Name of the Template associated with this Workflow. + Template string `json:"template,omitempty"` + + // Name of the Hardware associated with this workflow. + Hardware string `json:"hardware,omitempty"` +} + +// WorkflowStatus defines the observed state of Workflow. +type WorkflowStatus struct { + // State is the state of the workflow in Tinkerbell. + State string `json:"state,omitempty"` + + // Data is the populated Workflow Data in Tinkerbell. + Data string `json:"data,omitempty"` +} + +// +kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=workflows,scope=Cluster,categories=tinkerbell +// +kubebuilder:storageversion + +// Workflow is the Schema for the Workflows API. +type Workflow struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec WorkflowSpec `json:"spec,omitempty"` + Status WorkflowStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// WorkflowList contains a list of Workflows. +type WorkflowList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Workflow `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Workflow{}, &WorkflowList{}) +} diff --git a/tink/api/v1alpha1/zz_generated.deepcopy.go b/tink/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..3ae13a04 --- /dev/null +++ b/tink/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,292 @@ +// +build !ignore_autogenerated + +/* +Copyright 2020 The Kubernetes 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Hardware) DeepCopyInto(out *Hardware) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hardware. +func (in *Hardware) DeepCopy() *Hardware { + if in == nil { + return nil + } + out := new(Hardware) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Hardware) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HardwareList) DeepCopyInto(out *HardwareList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Hardware, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HardwareList. +func (in *HardwareList) DeepCopy() *HardwareList { + if in == nil { + return nil + } + out := new(HardwareList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HardwareList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HardwareSpec) DeepCopyInto(out *HardwareSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HardwareSpec. +func (in *HardwareSpec) DeepCopy() *HardwareSpec { + if in == nil { + return nil + } + out := new(HardwareSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HardwareStatus) DeepCopyInto(out *HardwareStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HardwareStatus. +func (in *HardwareStatus) DeepCopy() *HardwareStatus { + if in == nil { + return nil + } + out := new(HardwareStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Template) DeepCopyInto(out *Template) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Template. +func (in *Template) DeepCopy() *Template { + if in == nil { + return nil + } + out := new(Template) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Template) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateList) DeepCopyInto(out *TemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Template, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateList. +func (in *TemplateList) DeepCopy() *TemplateList { + if in == nil { + return nil + } + out := new(TemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateSpec) DeepCopyInto(out *TemplateSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSpec. +func (in *TemplateSpec) DeepCopy() *TemplateSpec { + if in == nil { + return nil + } + out := new(TemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateStatus) DeepCopyInto(out *TemplateStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateStatus. +func (in *TemplateStatus) DeepCopy() *TemplateStatus { + if in == nil { + return nil + } + out := new(TemplateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Workflow) DeepCopyInto(out *Workflow) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Workflow. +func (in *Workflow) DeepCopy() *Workflow { + if in == nil { + return nil + } + out := new(Workflow) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Workflow) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowList) DeepCopyInto(out *WorkflowList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Workflow, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowList. +func (in *WorkflowList) DeepCopy() *WorkflowList { + if in == nil { + return nil + } + out := new(WorkflowList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkflowList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowSpec) DeepCopyInto(out *WorkflowSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowSpec. +func (in *WorkflowSpec) DeepCopy() *WorkflowSpec { + if in == nil { + return nil + } + out := new(WorkflowSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkflowStatus) DeepCopyInto(out *WorkflowStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStatus. +func (in *WorkflowStatus) DeepCopy() *WorkflowStatus { + if in == nil { + return nil + } + out := new(WorkflowStatus) + in.DeepCopyInto(out) + return out +} diff --git a/tink/controllers/template_controller.go b/tink/controllers/template_controller.go new file mode 100644 index 00000000..7a4860eb --- /dev/null +++ b/tink/controllers/template_controller.go @@ -0,0 +1,104 @@ +/* +Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var ErrNotImplemented = fmt.Errorf("not implemented") + +type TemplateReconciler struct { + client.Client + Log logr.Logger + Recorder record.EventRecorder + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=tinkerbell.org,resources=templates,verbs=get;list;watch;create;update;patch;delete +func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + ctx := context.Background() + logger := r.Log.WithValues("template", req.NamespacedName.Name) + + // Fetch the template. + template := &tinkv1alpha1.Template{} + if err := r.Get(ctx, req.NamespacedName, template); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + + return ctrl.Result{}, fmt.Errorf("getting template: %w", err) + } + + // Add finalizer first if not exist to avoid the race condition between init and delete + if !controllerutil.ContainsFinalizer(template, tinkv1alpha1.TemplateFinalizer) { + before := template.DeepCopy() + controllerutil.AddFinalizer(template, tinkv1alpha1.TemplateFinalizer) + + if err := r.Client.Patch(ctx, template, client.MergeFrom(before)); err != nil { + logger.Error(err, "Failed to add finalizer to template") + + return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) + } + + return ctrl.Result{}, nil + } + + // Handle deleted clusters. + if !template.DeletionTimestamp.IsZero() { + return r.reconcileDelete(template) + } + + return r.reconcileNormal(template) +} + +func (r *TemplateReconciler) reconcileNormal(template *tinkv1alpha1.Template) (ctrl.Result, error) { + logger := r.Log.WithValues("template", template.Name) + err := ErrNotImplemented + + logger.Error(err, "Not yet implemented") + + return ctrl.Result{}, err +} + +func (r *TemplateReconciler) reconcileDelete(template *tinkv1alpha1.Template) (ctrl.Result, error) { + logger := r.Log.WithValues("template", template.Name) + err := ErrNotImplemented + + logger.Error(err, "Not yet implemented") + + // controllerutil.RemoveFinalizer(template, tinkv1alpha1.TemplateFinalizer) + + return ctrl.Result{}, err +} + +func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&tinkv1alpha1.Template{}). + Complete(r) +} From cfb3e70ff97506ad1d723c3c8a81ec7f80519880 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Thu, 10 Dec 2020 13:15:16 -0500 Subject: [PATCH 02/15] updates to templates --- config/crd/kustomization.yaml | 3 +++ templates/cluster-template.yaml | 15 --------------- templates/clusterctl-template.yaml | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index c9f34db6..fabf9a7a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -7,6 +7,9 @@ resources: - bases/infrastructure.cluster.x-k8s.io_tinkerbellclusters.yaml - bases/infrastructure.cluster.x-k8s.io_tinkerbellmachines.yaml - bases/infrastructure.cluster.x-k8s.io_tinkerbellmachinetemplates.yaml +- bases/tinkerbell.org_hardware.yaml +- bases/tinkerbell.org_templates.yaml +- bases/tinkerbell.org_workflows.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/templates/cluster-template.yaml b/templates/cluster-template.yaml index b43c510e..70c47c07 100644 --- a/templates/cluster-template.yaml +++ b/templates/cluster-template.yaml @@ -11,20 +11,8 @@ spec: name: "${CLUSTER_NAME}-control-plane" kubeadmConfigSpec: initConfiguration: - nodeRegistration: - kubeletExtraArgs: - cloud-provider: external clusterConfiguration: - apiServer: - extraArgs: - cloud-provider: external - controllerManager: - extraArgs: - cloud-provider: external joinConfiguration: - nodeRegistration: - kubeletExtraArgs: - cloud-provider: external --- apiVersion: infrastructure.cluster.x-k8s.io/v1alpha3 kind: TinkerbellMachineTemplate @@ -109,6 +97,3 @@ spec: template: spec: joinConfiguration: - nodeRegistration: - kubeletExtraArgs: - cloud-provider: external diff --git a/templates/clusterctl-template.yaml b/templates/clusterctl-template.yaml index 979fe7a5..1e2b4229 100644 --- a/templates/clusterctl-template.yaml +++ b/templates/clusterctl-template.yaml @@ -1,4 +1,4 @@ providers: -- name: packet +- name: tinkerbell url: URL type: InfrastructureProvider From 24842b1f14d5e39c9a37cc3cc8d08220aeaf66dd Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Thu, 10 Dec 2020 17:12:30 -0500 Subject: [PATCH 03/15] Start building out template controller --- config/rbac/role.yaml | 39 +++ go.mod | 3 +- go.sum | 370 +++++++++++++++++++++ tilt-provider.json | 2 +- tink/api/v1alpha1/hardware_types.go | 10 +- tink/api/v1alpha1/template_types.go | 3 +- tink/api/v1alpha1/workflow_types.go | 10 +- tink/api/v1alpha1/zz_generated.deepcopy.go | 7 +- tink/controllers/hardware_controller.go | 108 ++++++ tink/controllers/template_controller.go | 142 ++++++-- tink/controllers/workflow_controller.go | 108 ++++++ 11 files changed, 768 insertions(+), 34 deletions(-) create mode 100644 tink/controllers/hardware_controller.go create mode 100644 tink/controllers/workflow_controller.go diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 94432f28..ab99f1e0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -72,3 +72,42 @@ rules: - get - patch - update +- apiGroups: + - tinkerbell.org + resources: + - hardware + - hardware/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - tinkerbell.org + resources: + - templates + - templates/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - tinkerbell.org + resources: + - workflows + - workflows/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/go.mod b/go.mod index 94b8deaa..9d73661f 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ go 1.15 require ( github.com/go-logr/logr v0.1.0 + github.com/tinkerbell/tink v0.0.0-20201210163923-6d9159b63857 k8s.io/api v0.17.16 k8s.io/apimachinery v0.17.16 k8s.io/client-go v0.17.16 - k8s.io/utils v0.0.0-20201110183641-67b214c5f920 // indirect + k8s.io/utils v0.0.0-20201110183641-67b214c5f920 sigs.k8s.io/cluster-api v0.3.12 sigs.k8s.io/controller-runtime v0.5.14 ) diff --git a/go.sum b/go.sum index 3f344251..23949702 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,17 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= @@ -10,9 +21,12 @@ github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxB github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -20,38 +34,64 @@ github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alessio/shellescape v0.0.0-20190409004728-b115ca0f9053/go.mod h1:xW8sBma2LE3QxFSzCnH9qe6gAE2yO9GvQaWwX89HxbE= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/briandowns/spinner v1.8.0/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ= github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/coredns/corefile-migration v1.0.11/go.mod h1:RMy/mXdeDlYwzt0vdMEJvT2hGJ2I86/eO0UdXmH9XNI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -62,17 +102,22 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20191212201129-5f9f41018e9d/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= @@ -80,28 +125,43 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3 github.com/drone/envsubst v1.0.3-0.20200709223903-efdb65b94e5a/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.8.0/go.mod h1:3l45GVGkyrnYNl9HoIjnp2NnNWvh6hLAqD8yTfGjnw8= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54= @@ -149,32 +209,49 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/flect v0.2.2 h1:PAVD7sp0KOdfswjAw9BpLCU9hXo7wFSzgpQ+zNeks/A= github.com/gobuffalo/flect v0.2.2/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -183,6 +260,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= @@ -191,36 +271,73 @@ github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.15.2 h1:HC+hWRWf+v5zTMPyoaYTKIJih+4sd4XRWmj0qlG87Co= +github.com/grpc-ecosystem/grpc-gateway v1.15.2/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -232,24 +349,34 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.1-0.20191011153232-f91d3411e481/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -260,14 +387,29 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -276,76 +418,135 @@ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lN github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/packethost/pkg v0.0.0-20200903155310-0433e0605550/go.mod h1:GSv7cTtIjns4yc0pyajaM1RE/KE4djJONoblFIRDrxA= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -353,6 +554,7 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.1-0.20200713175500-884edc58ad08/go.mod h1:yk5b0mALVusDL5fMM6Rd1wgnoO5jUPhwsQ6LQAJTidQ= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -362,6 +564,12 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stormcat24/protodep v0.0.0-20200505140716-b02c9ba62816/go.mod h1:mBd5PI4uI6NkqJpCyiWiYzWyTFs4QRDss/JTMC2b4kc= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -370,57 +578,116 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tinkerbell/tink v0.0.0-20201210163923-6d9159b63857 h1:xQ+JJaD1QBPYerQtNX4ALQz4MljF1Nnq0BPfWwDxlKI= +github.com/tinkerbell/tink v0.0.0-20201210163923-6d9159b63857/go.mod h1:VuZ6LdWgAvyrtkqjnjePmjVXmwZyCO1dv4W/qFODBzc= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -429,15 +696,24 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -449,30 +725,47 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -485,6 +778,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -496,49 +790,111 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200921190806-0f52b63a40e8 h1:dAKTXMfGtKOr5ihvoK3V0Noq/SbFsD2XX0LJ19/ec/w= +golang.org/x/tools v0.0.0-20200921190806-0f52b63a40e8/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.0.1 h1:xyiBuvkD2g5n7cYzx6u2sxQvsAy4QJsZFCzGVdzOXZ0= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201026171402-d4b8fe4fd877 h1:d4k3uIU763E31Rk4UZPA47oOoBymMsDImV3U4mGhX9E= +google.golang.org/genproto v0.0.0-20201026171402-d4b8fe4fd877/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -546,21 +902,33 @@ gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.17.9/go.mod h1:avJJAA1fSV6tnbCGW2K+S+ilDFW7WpNr5BScoiZ1M1U= k8s.io/api v0.17.16 h1:whKfZJJp9m5fklRnlvO8+mJzpXat0gX0n+90d1hWTu0= k8s.io/api v0.17.16/go.mod h1:W8uKRxJeYRlAbWuk4CZv6BzuC7KuZnB6bSTPI7Pi8no= @@ -597,6 +965,7 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/cluster-api v0.3.12 h1:iogA3mXvwRfNU3ZCZXbqM1ZVWUxtWobgu3zRd5IkMTM= sigs.k8s.io/cluster-api v0.3.12/go.mod h1:KuUGKyD9+8TwYIcwoqDaScWKsee74/bpPo2sqQMwy8o= sigs.k8s.io/controller-runtime v0.5.14 h1:lmoRaPvLg9877ZOnjFivjtyIdqyLbWfcCEilxHXTEj4= @@ -607,3 +976,4 @@ sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7m sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/tilt-provider.json b/tilt-provider.json index 7234da01..a0280d25 100644 --- a/tilt-provider.json +++ b/tilt-provider.json @@ -3,7 +3,7 @@ "config": { "image": "quay.io/tinkerbell/cluster-api-provider-tinkerbell", "live_reload_deps": [ - "main.go", "go.mod", "go.sum", "api", "controllers" + "main.go", "go.mod", "go.sum", "api", "controllers", "tink" ] } } diff --git a/tink/api/v1alpha1/hardware_types.go b/tink/api/v1alpha1/hardware_types.go index 1596479c..351353ea 100644 --- a/tink/api/v1alpha1/hardware_types.go +++ b/tink/api/v1alpha1/hardware_types.go @@ -20,9 +20,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// HardwareIDAnnotation is used by the controller to store the -// ID assigned to the workflow by Tinkerbell. -const HardwareIDAnnotation = "hardware.tinkerbell.org/id" +const ( + // HardwareIDAnnotation is used by the controller to store the + // ID assigned to the workflow by Tinkerbell. + HardwareIDAnnotation = "hardware.tinkerbell.org/id" + + HardwareFinalizer = "hardware.tinkerbell.org" +) // HardwareSpec defines the desired state of Hardware. type HardwareSpec struct { diff --git a/tink/api/v1alpha1/template_types.go b/tink/api/v1alpha1/template_types.go index e9104830..3a5f29fb 100644 --- a/tink/api/v1alpha1/template_types.go +++ b/tink/api/v1alpha1/template_types.go @@ -30,7 +30,8 @@ const ( // TemplateSpec defines the desired state of Template. type TemplateSpec struct { - Data string `json:"data,omitempty"` + // +optional + Data *string `json:"data,omitempty"` } // TemplateStatus defines the observed state of Template. diff --git a/tink/api/v1alpha1/workflow_types.go b/tink/api/v1alpha1/workflow_types.go index 7822f816..622fae52 100644 --- a/tink/api/v1alpha1/workflow_types.go +++ b/tink/api/v1alpha1/workflow_types.go @@ -20,9 +20,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// WorkflowIDAnnotation is used by the controller to store the -// ID assigned to the workflow by Tinkerbell. -const WorkflowIDAnnotation = "workflow.tinkerbell.org/id" +const ( + // WorkflowIDAnnotation is used by the controller to store the + // ID assigned to the workflow by Tinkerbell. + WorkflowIDAnnotation = "workflow.tinkerbell.org/id" + + WorkflowFinalizer = "workflow.tinkerbell.org" +) // WorkflowSpec defines the desired state of Workflow. type WorkflowSpec struct { diff --git a/tink/api/v1alpha1/zz_generated.deepcopy.go b/tink/api/v1alpha1/zz_generated.deepcopy.go index 3ae13a04..0cbf9f19 100644 --- a/tink/api/v1alpha1/zz_generated.deepcopy.go +++ b/tink/api/v1alpha1/zz_generated.deepcopy.go @@ -118,7 +118,7 @@ func (in *Template) DeepCopyInto(out *Template) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -175,6 +175,11 @@ func (in *TemplateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplateSpec) DeepCopyInto(out *TemplateSpec) { *out = *in + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSpec. diff --git a/tink/controllers/hardware_controller.go b/tink/controllers/hardware_controller.go new file mode 100644 index 00000000..0bd03287 --- /dev/null +++ b/tink/controllers/hardware_controller.go @@ -0,0 +1,108 @@ +/* +Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/tink/protos/hardware" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// HardwareReconciler implements Reconciler interface by managing Tinkerbell hardware. +type HardwareReconciler struct { + client.Client + HardwareClient hardware.HardwareServiceClient + Log logr.Logger + Recorder record.EventRecorder + Scheme *runtime.Scheme +} + +// SetupWithManager configures reconciler with a given manager. +func (r *HardwareReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&tinkv1alpha1.Hardware{}). + Complete(r) +} + +// +kubebuilder:rbac:groups=tinkerbell.org,resources=hardware;hardware/status,verbs=get;list;watch;create;update;patch;delete + +// Reconcile ensures state of Tinkerbell hardware. +func (r *HardwareReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + ctx := context.Background() + logger := r.Log.WithValues("hardware", req.NamespacedName.Name) + + // Fetch the hardware. + hardware := &tinkv1alpha1.Hardware{} + if err := r.Get(ctx, req.NamespacedName, hardware); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + + return ctrl.Result{}, fmt.Errorf("getting hardware: %w", err) + } + + // Add finalizer first if not exist to avoid the race condition between init and delete + if !controllerutil.ContainsFinalizer(hardware, tinkv1alpha1.HardwareFinalizer) { + before := hardware.DeepCopy() + controllerutil.AddFinalizer(hardware, tinkv1alpha1.HardwareFinalizer) + + if err := r.Client.Patch(ctx, hardware, client.MergeFrom(before)); err != nil { + logger.Error(err, "Failed to add finalizer to hardware") + + return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) + } + + return ctrl.Result{}, nil + } + + // Handle deleted hardware. + if !hardware.DeletionTimestamp.IsZero() { + return r.reconcileDelete(hardware) + } + + return r.reconcileNormal(hardware) +} + +func (r *HardwareReconciler) reconcileNormal(hardware *tinkv1alpha1.Hardware) (ctrl.Result, error) { + logger := r.Log.WithValues("hardware", hardware.Name) + err := ErrNotImplemented + + logger.Error(err, "Not yet implemented") + + return ctrl.Result{}, err +} + +func (r *HardwareReconciler) reconcileDelete(hardware *tinkv1alpha1.Hardware) (ctrl.Result, error) { + logger := r.Log.WithValues("hardware", hardware.Name) + err := ErrNotImplemented + + logger.Error(err, "Not yet implemented") + + // controllerutil.RemoveFinalizer(hardware, tinkv1alpha1.HardwareFinalizer) + + return ctrl.Result{}, err +} diff --git a/tink/controllers/template_controller.go b/tink/controllers/template_controller.go index 7a4860eb..d1ff83ea 100644 --- a/tink/controllers/template_controller.go +++ b/tink/controllers/template_controller.go @@ -23,9 +23,10 @@ import ( "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/tink/protos/template" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -33,14 +34,24 @@ import ( var ErrNotImplemented = fmt.Errorf("not implemented") +// TemplateReconciler implements Reconciler interface by managing Tinkerbell templates. type TemplateReconciler struct { client.Client - Log logr.Logger - Recorder record.EventRecorder - Scheme *runtime.Scheme + TemplateClient template.TemplateServiceClient + Log logr.Logger + Scheme *runtime.Scheme } -// +kubebuilder:rbac:groups=tinkerbell.org,resources=templates,verbs=get;list;watch;create;update;patch;delete +// SetupWithManager configures reconciler with a given manager. +func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&tinkv1alpha1.Template{}). + Complete(r) +} + +// +kubebuilder:rbac:groups=tinkerbell.org,resources=templates;templates/status,verbs=get;list;watch;create;update;patch;delete + +// Reconcile ensures state of Tinkerbell templates. func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() logger := r.Log.WithValues("template", req.NamespacedName.Name) @@ -69,36 +80,119 @@ func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, nil } - // Handle deleted clusters. + // Handle deleted templates. if !template.DeletionTimestamp.IsZero() { - return r.reconcileDelete(template) + return r.reconcileDelete(ctx, template) } - return r.reconcileNormal(template) + return r.reconcileNormal(ctx, template) } -func (r *TemplateReconciler) reconcileNormal(template *tinkv1alpha1.Template) (ctrl.Result, error) { - logger := r.Log.WithValues("template", template.Name) - err := ErrNotImplemented +func (r *TemplateReconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { + // Create a patch for use later + patch := client.MergeFrom(t.DeepCopy()) - logger.Error(err, "Not yet implemented") + logger := r.Log.WithValues("template", t.Name) - return ctrl.Result{}, err -} + templateRequest := &template.GetRequest{} + + if t.Annotations == nil { + t.Annotations = map[string]string{} + } + + if id, ok := t.Annotations[tinkv1alpha1.TemplateIDAnnotation]; ok && id != "" { + // We've already recorded the ID, so we should prefer to use that. + templateRequest.GetBy = &template.GetRequest_Id{Id: id} + } else { + // We don't have an ID, so try to get the template by name. + // TODO: this will need to be improved for multitenancy support. + templateRequest.GetBy = &template.GetRequest_Name{Name: t.Name} + } + + tinkTemplate, err := r.TemplateClient.GetTemplate(ctx, templateRequest) + if err != nil { + // TODO: Tinkerbell should return some type of status that is easier to handle + // than parsing for this specific error message. + if err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { + logger.Error(err, "Failed to get template from Tinkerbell") + return ctrl.Result{}, err + } + + // We should create the template + tinkTemplate = &template.WorkflowTemplate{ + Name: t.Name, + Data: pointer.StringPtrDerefOr(t.Spec.Data, ""), + } + resp, err := r.TemplateClient.CreateTemplate(ctx, tinkTemplate) + if err != nil { + logger.Error(err, "Failed to create template in Tinkerbell") + return ctrl.Result{}, err + } + tinkTemplate.Id = resp.GetId() + } + + // If the data is specified and different than what is stored in Tinkerbell, + // update the template in Tinkerbell + if t.Spec.Data != nil && *t.Spec.Data != tinkTemplate.GetData() { + tinkTemplate.Data = *t.Spec.Data + if _, err := r.TemplateClient.UpdateTemplate(ctx, tinkTemplate); err != nil { + logger.Error(err, "Failed to update template in Tinkerbell") + return ctrl.Result{}, err + } + } -func (r *TemplateReconciler) reconcileDelete(template *tinkv1alpha1.Template) (ctrl.Result, error) { - logger := r.Log.WithValues("template", template.Name) - err := ErrNotImplemented + // Ensure that we populate the ID + t.Annotations[tinkv1alpha1.TemplateIDAnnotation] = tinkTemplate.Id - logger.Error(err, "Not yet implemented") + // If data was not specified, we are importing a pre-existing resource and should + // populate it from Tinkerbell + if t.Spec.Data == nil { + t.Spec.Data = pointer.StringPtr(tinkTemplate.GetData()) + } - // controllerutil.RemoveFinalizer(template, tinkv1alpha1.TemplateFinalizer) + if err := r.Client.Patch(ctx, t, patch); err != nil { + logger.Error(err, "Failed to patch template") + return ctrl.Result{}, err + } - return ctrl.Result{}, err + return ctrl.Result{}, nil } -func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&tinkv1alpha1.Template{}). - Complete(r) +func (r *TemplateReconciler) reconcileDelete(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { + // Create a patch for use later + patch := client.MergeFrom(t.DeepCopy()) + + logger := r.Log.WithValues("template", t.Name) + + if t.Annotations == nil { + t.Annotations = map[string]string{} + } + + templateRequest := &template.GetRequest{} + + if id, ok := t.Annotations[tinkv1alpha1.TemplateIDAnnotation]; ok && id != "" { + // We've already recorded the ID, so we should prefer to use that. + templateRequest.GetBy = &template.GetRequest_Id{Id: id} + } else { + // We don't have an ID, so try to get the template by name. + // TODO: this will need to be improved for multitenancy support. + templateRequest.GetBy = &template.GetRequest_Name{Name: t.Name} + } + + _, err := r.TemplateClient.DeleteTemplate(ctx, templateRequest) + // TODO: Tinkerbell should return some type of status that is easier to handle + // than parsing for this specific error message. + if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { + // If it doesn't exist, then return without an error + logger.Error(err, "Failed to delete template from Tinkerbell") + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(t, tinkv1alpha1.TemplateFinalizer) + if err := r.Client.Patch(ctx, t, patch); err != nil { + logger.Error(err, "Failed to patch template") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil } diff --git a/tink/controllers/workflow_controller.go b/tink/controllers/workflow_controller.go new file mode 100644 index 00000000..b19f88aa --- /dev/null +++ b/tink/controllers/workflow_controller.go @@ -0,0 +1,108 @@ +/* +Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/tink/protos/workflow" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// WorkflowReconciler implements Reconciler interface by managing Tinkerbell workflows. +type WorkflowReconciler struct { + client.Client + WorkflowClient workflow.WorkflowServiceClient + Log logr.Logger + Recorder record.EventRecorder + Scheme *runtime.Scheme +} + +// SetupWithManager configures reconciler with a given manager. +func (r *WorkflowReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&tinkv1alpha1.Workflow{}). + Complete(r) +} + +// +kubebuilder:rbac:groups=tinkerbell.org,resources=workflows;workflows/status,verbs=get;list;watch;create;update;patch;delete + +// Reconcile ensures state of Tinkerbell workflows. +func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + ctx := context.Background() + logger := r.Log.WithValues("workflow", req.NamespacedName.Name) + + // Fetch the workflow. + workflow := &tinkv1alpha1.Workflow{} + if err := r.Get(ctx, req.NamespacedName, workflow); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + + return ctrl.Result{}, fmt.Errorf("getting workflow: %w", err) + } + + // Add finalizer first if not exist to avoid the race condition between init and delete + if !controllerutil.ContainsFinalizer(workflow, tinkv1alpha1.WorkflowFinalizer) { + before := workflow.DeepCopy() + controllerutil.AddFinalizer(workflow, tinkv1alpha1.WorkflowFinalizer) + + if err := r.Client.Patch(ctx, workflow, client.MergeFrom(before)); err != nil { + logger.Error(err, "Failed to add finalizer to workflow") + + return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) + } + + return ctrl.Result{}, nil + } + + // Handle deleted wokflows. + if !workflow.DeletionTimestamp.IsZero() { + return r.reconcileDelete(workflow) + } + + return r.reconcileNormal(workflow) +} + +func (r *WorkflowReconciler) reconcileNormal(workflow *tinkv1alpha1.Workflow) (ctrl.Result, error) { + logger := r.Log.WithValues("workflow", workflow.Name) + err := ErrNotImplemented + + logger.Error(err, "Not yet implemented") + + return ctrl.Result{}, err +} + +func (r *WorkflowReconciler) reconcileDelete(workflow *tinkv1alpha1.Workflow) (ctrl.Result, error) { + logger := r.Log.WithValues("workflow", workflow.Name) + err := ErrNotImplemented + + logger.Error(err, "Not yet implemented") + + // controllerutil.RemoveFinalizer(workflow, tinkv1alpha1.WorkflowFinalizer) + + return ctrl.Result{}, err +} From c9ae7d6985f3351ac1e8a00f4d6841ff21315f19 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Fri, 11 Dec 2020 12:44:00 -0500 Subject: [PATCH 04/15] review feedback --- api/v1alpha3/doc.go | 1 + api/v1alpha3/groupversion_info.go | 1 - tink/api/v1alpha1/doc.go | 1 + tink/api/v1alpha1/groupversion_info.go | 1 - 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/v1alpha3/doc.go b/api/v1alpha3/doc.go index 8f663577..4903f43c 100644 --- a/api/v1alpha3/doc.go +++ b/api/v1alpha3/doc.go @@ -11,4 +11,5 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package v1alpha3 contains API Schema definitions for the infrastructure v1alpha3 API group. package v1alpha3 diff --git a/api/v1alpha3/groupversion_info.go b/api/v1alpha3/groupversion_info.go index e56e3f87..af53651f 100644 --- a/api/v1alpha3/groupversion_info.go +++ b/api/v1alpha3/groupversion_info.go @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1alpha3 contains API Schema definitions for the infrastructure v1alpha3 API group. // +kubebuilder:object:generate=true // +groupName=infrastructure.cluster.x-k8s.io package v1alpha3 diff --git a/tink/api/v1alpha1/doc.go b/tink/api/v1alpha1/doc.go index 5abc18bf..3f7c0025 100644 --- a/tink/api/v1alpha1/doc.go +++ b/tink/api/v1alpha1/doc.go @@ -11,4 +11,5 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package v1alpha1 contains API Schema definitions for the Tinkerbell v1alpha1 API group package v1alpha1 diff --git a/tink/api/v1alpha1/groupversion_info.go b/tink/api/v1alpha1/groupversion_info.go index 729cf09b..6ad9e194 100644 --- a/tink/api/v1alpha1/groupversion_info.go +++ b/tink/api/v1alpha1/groupversion_info.go @@ -11,7 +11,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1alpha1 contains API Schema definitions for the Tinkerbell v1alpha1 API group // +kubebuilder:object:generate=true // +groupName=tinkerbell.org package v1alpha1 From 38dd4d734af0ad57705edc0002d07574def6aa55 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Fri, 11 Dec 2020 17:36:28 -0500 Subject: [PATCH 05/15] Continue building out Tink types and controllers --- .../crd/bases/tinkerbell.org_workflows.yaml | 9 +- go.mod | 1 + tink/api/v1alpha1/hardware_types.go | 4 +- tink/api/v1alpha1/template_types.go | 2 +- tink/api/v1alpha1/workflow_types.go | 9 +- tink/controllers/common.go | 46 +++ tink/controllers/common_test.go | 87 +++++ tink/controllers/hardware_controller.go | 58 ++- tink/controllers/template_controller.go | 20 +- tink/controllers/template_controller_test.go | 357 ++++++++++++++++++ tink/controllers/workflow_controller.go | 61 +-- 11 files changed, 591 insertions(+), 63 deletions(-) create mode 100644 tink/controllers/common.go create mode 100644 tink/controllers/common_test.go create mode 100644 tink/controllers/template_controller_test.go diff --git a/config/crd/bases/tinkerbell.org_workflows.yaml b/config/crd/bases/tinkerbell.org_workflows.yaml index 68fc98e3..2dd5ca01 100644 --- a/config/crd/bases/tinkerbell.org_workflows.yaml +++ b/config/crd/bases/tinkerbell.org_workflows.yaml @@ -34,11 +34,14 @@ spec: spec: description: WorkflowSpec defines the desired state of Workflow. properties: - hardware: + hardwareRef: description: Name of the Hardware associated with this workflow. type: string - template: - description: Name of the Template associated with this Workflow. + hardwareTemplate: + description: HardwareTemplate is the template used for creating the workflow. + type: string + templateRef: + description: Name of the Template associated with this workflow. type: string type: object status: diff --git a/go.mod b/go.mod index 9d73661f..87cccb34 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/go-logr/logr v0.1.0 + github.com/onsi/gomega v1.10.1 github.com/tinkerbell/tink v0.0.0-20201210163923-6d9159b63857 k8s.io/api v0.17.16 k8s.io/apimachinery v0.17.16 diff --git a/tink/api/v1alpha1/hardware_types.go b/tink/api/v1alpha1/hardware_types.go index 351353ea..6fd447f4 100644 --- a/tink/api/v1alpha1/hardware_types.go +++ b/tink/api/v1alpha1/hardware_types.go @@ -22,7 +22,7 @@ import ( const ( // HardwareIDAnnotation is used by the controller to store the - // ID assigned to the workflow by Tinkerbell. + // ID assigned to the hardware by Tinkerbell. HardwareIDAnnotation = "hardware.tinkerbell.org/id" HardwareFinalizer = "hardware.tinkerbell.org" @@ -38,7 +38,7 @@ type HardwareStatus struct { // +kubebuilder:subresource:status // +kubebuilder:object:root=true -// +kubebuilder:resource:path=hardware,scope=Cluster,categories=tinkerbell +// +kubebuilder:resource:path=hardware,scope=Cluster,categories=tinkerbell,singular=hardware // +kubebuilder:storageversion // Hardware is the Schema for the Hardware API. diff --git a/tink/api/v1alpha1/template_types.go b/tink/api/v1alpha1/template_types.go index 3a5f29fb..c3e185ae 100644 --- a/tink/api/v1alpha1/template_types.go +++ b/tink/api/v1alpha1/template_types.go @@ -22,7 +22,7 @@ import ( const ( // TemplateIDAnnotation is used by the controller to store the - // ID assigned to the workflow by Tinkerbell. + // ID assigned to the template by Tinkerbell. TemplateIDAnnotation = "template.tinkerbell.org/id" TemplateFinalizer = "template.tinkerbell.org" diff --git a/tink/api/v1alpha1/workflow_types.go b/tink/api/v1alpha1/workflow_types.go index 622fae52..974197f9 100644 --- a/tink/api/v1alpha1/workflow_types.go +++ b/tink/api/v1alpha1/workflow_types.go @@ -30,11 +30,14 @@ const ( // WorkflowSpec defines the desired state of Workflow. type WorkflowSpec struct { - // Name of the Template associated with this Workflow. - Template string `json:"template,omitempty"` + // Name of the Template associated with this workflow. + TemplateRef string `json:"templateRef,omitempty"` // Name of the Hardware associated with this workflow. - Hardware string `json:"hardware,omitempty"` + HardwareRef string `json:"hardwareRef,omitempty"` + + // HardwareTemplate is the template used for creating the workflow. + HardwareTemplate string `json:"hardwareTemplate,omitempty"` } // WorkflowStatus defines the observed state of Workflow. diff --git a/tink/controllers/common.go b/tink/controllers/common.go new file mode 100644 index 00000000..480b9d01 --- /dev/null +++ b/tink/controllers/common.go @@ -0,0 +1,46 @@ +/* +Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. +package controllers + +import ( + "context" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type Object interface { + metav1.Object + runtime.Object +} + +func ensureFinalizer(ctx context.Context, c client.Client, logger logr.Logger, obj Object, finalizer string) error { + patch := client.MergeFrom(obj.DeepCopyObject()) + + controllerutil.AddFinalizer(obj, finalizer) + + if err := c.Patch(ctx, obj, patch); err != nil { + logger.Error(err, "Failed to add finalizer to template") + return err + } + + return nil +} diff --git a/tink/controllers/common_test.go b/tink/controllers/common_test.go new file mode 100644 index 00000000..7d36b1d5 --- /dev/null +++ b/tink/controllers/common_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. +package controllers + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func Test_ensureFinalizer(t *testing.T) { + g := NewWithT(t) + scheme := runtime.NewScheme() + + g.Expect(tinkv1alpha1.AddToScheme(scheme)).To(Succeed()) + + tests := []struct { + name string + in Object + finalizer string + wantErr bool + }{ + { + name: "Adds finalizer when not present", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: tinkv1alpha1.TemplateSpec{}, + }, + finalizer: "my-test-finalizer", + wantErr: false, + }, + { + name: "Finalizer already exists", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Finalizers: []string{"my-test-finalizer"}, + }, + Spec: tinkv1alpha1.TemplateSpec{}, + }, + finalizer: "my-test-finalizer", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + fakeClient := fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopyObject()) + + err := ensureFinalizer(context.Background(), fakeClient, log.Log, tt.in, tt.finalizer) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + + actual := &tinkv1alpha1.Template{} + key := client.ObjectKey{Name: tt.in.GetName()} + g.Expect(fakeClient.Get(context.Background(), key, actual)).To(Succeed()) + g.Expect(actual.Finalizers).To(ContainElement(tt.finalizer)) + }) + } +} diff --git a/tink/controllers/hardware_controller.go b/tink/controllers/hardware_controller.go index 0bd03287..c85b70c9 100644 --- a/tink/controllers/hardware_controller.go +++ b/tink/controllers/hardware_controller.go @@ -62,26 +62,19 @@ func (r *HardwareReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, nil } + logger.Error(err, "Failed to get hardware") return ctrl.Result{}, fmt.Errorf("getting hardware: %w", err) } - // Add finalizer first if not exist to avoid the race condition between init and delete - if !controllerutil.ContainsFinalizer(hardware, tinkv1alpha1.HardwareFinalizer) { - before := hardware.DeepCopy() - controllerutil.AddFinalizer(hardware, tinkv1alpha1.HardwareFinalizer) - - if err := r.Client.Patch(ctx, hardware, client.MergeFrom(before)); err != nil { - logger.Error(err, "Failed to add finalizer to hardware") - - return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) - } - - return ctrl.Result{}, nil + // Ensure that we add the finalizer to the resource + err := ensureFinalizer(ctx, r.Client, logger, hardware, tinkv1alpha1.HardwareFinalizer) + if err != nil { + return ctrl.Result{}, err } // Handle deleted hardware. if !hardware.DeletionTimestamp.IsZero() { - return r.reconcileDelete(hardware) + return r.reconcileDelete(ctx, hardware) } return r.reconcileNormal(hardware) @@ -96,13 +89,40 @@ func (r *HardwareReconciler) reconcileNormal(hardware *tinkv1alpha1.Hardware) (c return ctrl.Result{}, err } -func (r *HardwareReconciler) reconcileDelete(hardware *tinkv1alpha1.Hardware) (ctrl.Result, error) { - logger := r.Log.WithValues("hardware", hardware.Name) - err := ErrNotImplemented +func (r *HardwareReconciler) reconcileDelete(ctx context.Context, h *tinkv1alpha1.Hardware) (ctrl.Result, error) { + // Create a patch for use later + patch := client.MergeFrom(h.DeepCopy()) - logger.Error(err, "Not yet implemented") + logger := r.Log.WithValues("hardware", h.Name) - // controllerutil.RemoveFinalizer(hardware, tinkv1alpha1.HardwareFinalizer) + if h.Annotations == nil { + h.Annotations = map[string]string{} + } - return ctrl.Result{}, err + id, ok := h.Annotations[tinkv1alpha1.HardwareIDAnnotation] + if !ok { + // TODO: figure out how to lookup a hardware without an ID + logger.Error(ErrNotImplemented, "Unable to delete a hardware without having recorded the ID") + return ctrl.Result{}, ErrNotImplemented + } + + hardwareRequest := &hardware.DeleteRequest{ + Id: id, + } + + _, err := r.HardwareClient.Delete(ctx, hardwareRequest) + // TODO: Tinkerbell should return some type of status that is easier to handle + // than parsing for this specific error message. + if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { + logger.Error(err, "Failed to delete hardware from Tinkerbell") + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(h, tinkv1alpha1.HardwareFinalizer) + if err := r.Client.Patch(ctx, h, patch); err != nil { + logger.Error(err, "Failed to patch hardware") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil } diff --git a/tink/controllers/template_controller.go b/tink/controllers/template_controller.go index d1ff83ea..8e932730 100644 --- a/tink/controllers/template_controller.go +++ b/tink/controllers/template_controller.go @@ -63,21 +63,14 @@ func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, nil } - return ctrl.Result{}, fmt.Errorf("getting template: %w", err) + logger.Error(err, "Failed to get template") + return ctrl.Result{}, err } - // Add finalizer first if not exist to avoid the race condition between init and delete - if !controllerutil.ContainsFinalizer(template, tinkv1alpha1.TemplateFinalizer) { - before := template.DeepCopy() - controllerutil.AddFinalizer(template, tinkv1alpha1.TemplateFinalizer) - - if err := r.Client.Patch(ctx, template, client.MergeFrom(before)); err != nil { - logger.Error(err, "Failed to add finalizer to template") - - return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) - } - - return ctrl.Result{}, nil + // Ensure that we add the finalizer to the resource + err := ensureFinalizer(ctx, r.Client, logger, template, tinkv1alpha1.TemplateFinalizer) + if err != nil { + return ctrl.Result{}, err } // Handle deleted templates. @@ -183,7 +176,6 @@ func (r *TemplateReconciler) reconcileDelete(ctx context.Context, t *tinkv1alpha // TODO: Tinkerbell should return some type of status that is easier to handle // than parsing for this specific error message. if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { - // If it doesn't exist, then return without an error logger.Error(err, "Failed to delete template from Tinkerbell") return ctrl.Result{}, err } diff --git a/tink/controllers/template_controller_test.go b/tink/controllers/template_controller_test.go new file mode 100644 index 00000000..f946721a --- /dev/null +++ b/tink/controllers/template_controller_test.go @@ -0,0 +1,357 @@ +/* +Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. +package controllers + +import ( + "context" + "errors" + "testing" + + . "github.com/onsi/gomega" + tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/tink/protos/template" + "google.golang.org/grpc" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const helloWorldTemplate = `version: "0.1" +name: hello_world_workflow +global_timeout: 600 +tasks: + - name: "hello world" + worker: "{{.device_1}}" + actions: + - name: "hello_world" + image: hello-world + timeout: 60` + +type fakeTemplateServiceClient struct { + objs map[string]*template.WorkflowTemplate +} + +func newFakeTemplateServiceClient(objs ...*template.WorkflowTemplate) *fakeTemplateServiceClient { + f := &fakeTemplateServiceClient{objs: map[string]*template.WorkflowTemplate{}} + + for i, obj := range objs { + id := obj.GetId() + + if id == "" { + obj.Id = obj.GetName() + } + + f.objs[id] = objs[i] + } + + return f +} + +func (f *fakeTemplateServiceClient) CreateTemplate(ctx context.Context, in *template.WorkflowTemplate, opts ...grpc.CallOption) (*template.CreateResponse, error) { + id := in.GetId() + + if id == "" { + id = in.GetName() + + } + + if _, ok := f.objs[id]; ok { + return nil, errors.New("duplicate") + } + + f.objs[id] = &template.WorkflowTemplate{ + Id: id, + Name: in.GetName(), + Data: in.GetData(), + } + + return &template.CreateResponse{Id: id}, nil +} + +func (f *fakeTemplateServiceClient) GetTemplate(ctx context.Context, in *template.GetRequest, opts ...grpc.CallOption) (*template.WorkflowTemplate, error) { + id := in.GetId() + if id == "" { + id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + return f.objs[id], nil + } + + return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") +} + +func (f *fakeTemplateServiceClient) DeleteTemplate(ctx context.Context, in *template.GetRequest, opts ...grpc.CallOption) (*template.Empty, error) { + id := in.GetId() + if id == "" { + id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + delete(f.objs, id) + return &template.Empty{}, nil + } + + return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") +} + +func (f *fakeTemplateServiceClient) ListTemplates(ctx context.Context, in *template.ListRequest, opts ...grpc.CallOption) (template.TemplateService_ListTemplatesClient, error) { + return nil, errors.New("nobody home") +} + +func (f *fakeTemplateServiceClient) UpdateTemplate(ctx context.Context, in *template.WorkflowTemplate, opts ...grpc.CallOption) (*template.Empty, error) { + id := in.GetId() + + if id == "" { + in.Id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + f.objs[id].Data = in.GetData() + return &template.Empty{}, nil + } + + return nil, errors.New("nobody home") +} + +func TestTemplateReconciler_reconcileDelete(t *testing.T) { + g := NewWithT(t) + scheme := runtime.NewScheme() + now := metav1.Now() + + g.Expect(tinkv1alpha1.AddToScheme(scheme)).To(Succeed()) + + tests := []struct { + name string + in *tinkv1alpha1.Template + tinkObjs []*template.WorkflowTemplate + want ctrl.Result + wantErr bool + }{ + { + name: "successful delete by name", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + DeletionTimestamp: &now, + Finalizers: []string{tinkv1alpha1.TemplateFinalizer}, + }, + }, + tinkObjs: []*template.WorkflowTemplate{ + { + Id: "test", + Name: "test", + }, + }, + want: ctrl.Result{}, + wantErr: false, + }, + { + name: "successful delete by id", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + DeletionTimestamp: &now, + Finalizers: []string{tinkv1alpha1.TemplateFinalizer}, + Annotations: map[string]string{ + tinkv1alpha1.TemplateIDAnnotation: "testId", + }, + }, + }, + tinkObjs: []*template.WorkflowTemplate{ + { + Id: "testId", + Name: "test", + }, + }, + want: ctrl.Result{}, + wantErr: false, + }, + { + name: "template doesn't exist in tinkerbell", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + DeletionTimestamp: &now, + Finalizers: []string{tinkv1alpha1.TemplateFinalizer}, + Annotations: map[string]string{ + tinkv1alpha1.TemplateIDAnnotation: "testId", + }, + }, + }, + tinkObjs: []*template.WorkflowTemplate{}, + want: ctrl.Result{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + fakeTemplateClient := newFakeTemplateServiceClient(tt.tinkObjs...) + + r := &TemplateReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopy()), + TemplateClient: fakeTemplateClient, + Log: log.Log, + Scheme: scheme, + } + + got, err := r.reconcileDelete(context.Background(), tt.in) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(got).To(BeEquivalentTo(tt.want)) + + // Verify that the deletion happened in the fakeTemplateServiceClient + g.Expect(fakeTemplateClient.objs).NotTo(HaveKey(tt.in.Name)) + + // Check for absence of a finalizer since the fake client doesn't + // do automatic deletion + key := client.ObjectKey{Name: tt.in.Name} + after := &tinkv1alpha1.Template{} + err = r.Client.Get(context.Background(), key, after) + g.Expect(after.Finalizers).NotTo(ContainElement(tinkv1alpha1.TemplateFinalizer)) + }) + } +} + +func TestTemplateReconciler_reconcileNormal(t *testing.T) { + g := NewWithT(t) + scheme := runtime.NewScheme() + + g.Expect(tinkv1alpha1.AddToScheme(scheme)).To(Succeed()) + + tests := []struct { + name string + in *tinkv1alpha1.Template + tinkObjs []*template.WorkflowTemplate + want ctrl.Result + wantErr bool + }{ + { + name: "successful create", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }, + tinkObjs: nil, + want: ctrl.Result{}, + wantErr: false, + }, + { + name: "successful update", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1alpha1.TemplateIDAnnotation: "testId", + }, + }, + Spec: tinkv1alpha1.TemplateSpec{ + Data: pointer.StringPtr(helloWorldTemplate), + }, + }, + tinkObjs: []*template.WorkflowTemplate{ + { + Id: "testId", + Name: "test", + }, + }, + want: ctrl.Result{}, + wantErr: false, + }, + { + name: "successful adopt", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }, + tinkObjs: []*template.WorkflowTemplate{ + { + Id: "testId", + Name: "test", + Data: helloWorldTemplate, + }, + }, + want: ctrl.Result{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + fakeTemplateClient := newFakeTemplateServiceClient(tt.tinkObjs...) + + r := &TemplateReconciler{ + Client: fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopy()), + TemplateClient: fakeTemplateClient, + Log: log.Log, + Scheme: scheme, + } + + got, err := r.reconcileNormal(context.Background(), tt.in) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(got).To(BeEquivalentTo(tt.want)) + + // get the k8s resource from the fake client + k8sTemplate := &tinkv1alpha1.Template{} + key := client.ObjectKey{Name: tt.in.Name} + g.Expect(r.Client.Get(context.Background(), key, k8sTemplate)).To(Succeed()) + + // expect the k8s resource to have an id stored + g.Expect(k8sTemplate.GetAnnotations()).To(HaveKey(tinkv1alpha1.TemplateIDAnnotation)) + + id := k8sTemplate.Annotations[tinkv1alpha1.TemplateIDAnnotation] + + // Expect the id to be non-empty + g.Expect(id).NotTo(BeEmpty()) + + // Verify that the resource exists in the fakeTemplateServiceClient + g.Expect(fakeTemplateClient.objs).To(HaveKey(id)) + + // get the tink resource from the fake client + tinkTemplate := fakeTemplateClient.objs[id] + + // Verify the IDs match + g.Expect(tinkTemplate.Id, k8sTemplate.Annotations[tinkv1alpha1.TemplateIDAnnotation]) + + // Verify the Names match + g.Expect(tinkTemplate.Name).To(BeEquivalentTo(k8sTemplate.Name)) + + // Verify the Data matches + g.Expect(k8sTemplate.Spec.Data).NotTo(BeNil()) + g.Expect(tinkTemplate.Data).To(BeEquivalentTo(*k8sTemplate.Spec.Data)) + }) + } +} diff --git a/tink/controllers/workflow_controller.go b/tink/controllers/workflow_controller.go index b19f88aa..5d965b8c 100644 --- a/tink/controllers/workflow_controller.go +++ b/tink/controllers/workflow_controller.go @@ -19,7 +19,6 @@ package controllers import ( "context" - "fmt" "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" @@ -62,26 +61,19 @@ func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, nil } - return ctrl.Result{}, fmt.Errorf("getting workflow: %w", err) + logger.Error(err, "Failed to get workflow") + return ctrl.Result{}, err } - // Add finalizer first if not exist to avoid the race condition between init and delete - if !controllerutil.ContainsFinalizer(workflow, tinkv1alpha1.WorkflowFinalizer) { - before := workflow.DeepCopy() - controllerutil.AddFinalizer(workflow, tinkv1alpha1.WorkflowFinalizer) - - if err := r.Client.Patch(ctx, workflow, client.MergeFrom(before)); err != nil { - logger.Error(err, "Failed to add finalizer to workflow") - - return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %w", err) - } - - return ctrl.Result{}, nil + // Ensure that we add the finalizer to the resource + err := ensureFinalizer(ctx, r.Client, logger, workflow, tinkv1alpha1.WorkflowFinalizer) + if err != nil { + return ctrl.Result{}, err } // Handle deleted wokflows. if !workflow.DeletionTimestamp.IsZero() { - return r.reconcileDelete(workflow) + return r.reconcileDelete(ctx, workflow) } return r.reconcileNormal(workflow) @@ -96,13 +88,40 @@ func (r *WorkflowReconciler) reconcileNormal(workflow *tinkv1alpha1.Workflow) (c return ctrl.Result{}, err } -func (r *WorkflowReconciler) reconcileDelete(workflow *tinkv1alpha1.Workflow) (ctrl.Result, error) { - logger := r.Log.WithValues("workflow", workflow.Name) - err := ErrNotImplemented +func (r *WorkflowReconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { + // Create a patch for use later + patch := client.MergeFrom(w.DeepCopy()) - logger.Error(err, "Not yet implemented") + logger := r.Log.WithValues("workflow", w.Name) - // controllerutil.RemoveFinalizer(workflow, tinkv1alpha1.WorkflowFinalizer) + if w.Annotations == nil { + w.Annotations = map[string]string{} + } - return ctrl.Result{}, err + id, ok := w.Annotations[tinkv1alpha1.WorkflowIDAnnotation] + if !ok { + // TODO: figure out how to lookup a workflow without an ID + logger.Error(ErrNotImplemented, "Unable to delete a workflow without having recorded the ID") + return ctrl.Result{}, ErrNotImplemented + } + + workflowRequest := &workflow.GetRequest{ + Id: id, + } + + _, err := r.WorkflowClient.DeleteWorkflow(ctx, workflowRequest) + // TODO: Tinkerbell should return some type of status that is easier to handle + // than parsing for this specific error message. + if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { + logger.Error(err, "Failed to delete workflow from Tinkerbell") + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(w, tinkv1alpha1.WorkflowFinalizer) + if err := r.Client.Patch(ctx, w, patch); err != nil { + logger.Error(err, "Failed to patch workflow") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil } From 651ee322071c007fce26f3afeb37ead45261d7fa Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Mon, 14 Dec 2020 17:13:17 -0500 Subject: [PATCH 06/15] Refactor the shim a bit --- .gitignore | 1 + tink/controllers/common.go | 46 ------ tink/internal/client/client.go | 100 +++++++++++++ tink/internal/client/fake/client.go | 116 ++++++++++++++ tink/internal/client_test/client_test.go | 133 +++++++++++++++++ tink/internal/controllers/common/common.go | 79 ++++++++++ .../controllers/common_test}/common_test.go | 81 +++++++++- .../controllers/hardware/controller.go} | 38 +++-- .../controllers/template/controller.go} | 141 ++++++++++-------- .../template/controller_internal_test.go} | 116 ++------------ .../controllers/workflow/controller.go} | 23 ++- 11 files changed, 631 insertions(+), 243 deletions(-) delete mode 100644 tink/controllers/common.go create mode 100644 tink/internal/client/client.go create mode 100644 tink/internal/client/fake/client.go create mode 100644 tink/internal/client_test/client_test.go create mode 100644 tink/internal/controllers/common/common.go rename tink/{controllers => internal/controllers/common_test}/common_test.go (51%) rename tink/{controllers/hardware_controller.go => internal/controllers/hardware/controller.go} (72%) rename tink/{controllers/template_controller.go => internal/controllers/template/controller.go} (51%) rename tink/{controllers/template_controller_test.go => internal/controllers/template/controller_internal_test.go} (71%) rename tink/{controllers/workflow_controller.go => internal/controllers/workflow/controller.go} (84%) diff --git a/.gitignore b/.gitignore index fc4ac1d0..6f520c72 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # editor and IDE paraphernalia .idea +.vscode *.swp *.swo *~ diff --git a/tink/controllers/common.go b/tink/controllers/common.go deleted file mode 100644 index 480b9d01..00000000 --- a/tink/controllers/common.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2020 The Kubernetes 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 controllers contains controllers for Tinkerbell. -package controllers - -import ( - "context" - - "github.com/go-logr/logr" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -type Object interface { - metav1.Object - runtime.Object -} - -func ensureFinalizer(ctx context.Context, c client.Client, logger logr.Logger, obj Object, finalizer string) error { - patch := client.MergeFrom(obj.DeepCopyObject()) - - controllerutil.AddFinalizer(obj, finalizer) - - if err := c.Patch(ctx, obj, patch); err != nil { - logger.Error(err, "Failed to add finalizer to template") - return err - } - - return nil -} diff --git a/tink/internal/client/client.go b/tink/internal/client/client.go new file mode 100644 index 00000000..2bcb71fc --- /dev/null +++ b/tink/internal/client/client.go @@ -0,0 +1,100 @@ +/* +Copyright 2020 The Kubernetes 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 client contains a client wrapper for Tinkerbell. +package client + +import ( + "context" + "errors" + "fmt" + + "github.com/tinkerbell/tink/protos/template" +) + +// ErrNotFound is returned if a requested resource is not found. +var ErrNotFound = errors.New("resource not found") + +// Template client for Tinkerbell. +type Template struct { + client template.TemplateServiceClient +} + +// Get returns a Tinkerbell Template. +func (t *Template) Get(ctx context.Context, id, name string) (*template.WorkflowTemplate, error) { + req := &template.GetRequest{} + if id == "" { + req.GetBy = &template.GetRequest_Id{Id: id} + } else { + req.GetBy = &template.GetRequest_Name{Name: name} + } + + tinkTemplate, err := t.client.GetTemplate(ctx, req) + if err != nil { + // TODO: Tinkerbell should return some type of status that is easier to handle + // than parsing for this specific error message. + if err.Error() == "rpc error: code = Unknown desc = sql: no rows in result set" { + return nil, fmt.Errorf("template %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get template from Tinkerbell: %w", err) + } + + return tinkTemplate, nil +} + +// Update a Tinkerbell Template. +func (t *Template) Update(ctx context.Context, template *template.WorkflowTemplate) error { + if _, err := t.client.UpdateTemplate(ctx, template); err != nil { + return fmt.Errorf("failed to update template in Tinkerbell: %w", err) + } + + return nil +} + +// Create a Tinkerbell Template. +func (t *Template) Create(ctx context.Context, template *template.WorkflowTemplate) error { + resp, err := t.client.CreateTemplate(ctx, template) + if err != nil { + return fmt.Errorf("failed to create template in Tinkerbell: %w", err) + } + + template.Id = resp.GetId() + + return nil +} + +// Delete a Tinkerbell Template. +func (t *Template) Delete(ctx context.Context, id, name string) error { + req := &template.GetRequest{} + if id == "" { + req.GetBy = &template.GetRequest_Id{Id: id} + } else { + req.GetBy = &template.GetRequest_Name{Name: name} + } + + if _, err := t.client.DeleteTemplate(ctx, req); err != nil { + // TODO: Tinkerbell should return some type of status that is easier to handle + // than parsing for this specific error message. + if err.Error() == "rpc error: code = Unknown desc = sql: no rows in result set" { + return fmt.Errorf("template %w", ErrNotFound) + } + + return fmt.Errorf("failed to delete template from Tinkerbell: %w", err) + } + + return nil +} diff --git a/tink/internal/client/fake/client.go b/tink/internal/client/fake/client.go new file mode 100644 index 00000000..61a90806 --- /dev/null +++ b/tink/internal/client/fake/client.go @@ -0,0 +1,116 @@ +/* +Copyright 2020 The Kubernetes 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 fake contains a fake client wrapper for Tinkerbell. +package fake + +import ( + "context" + "errors" + + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/tink/protos/template" +) + +// Template is a fake client for Tinkerbell Templates. +type Template struct { + Objs map[string]*template.WorkflowTemplate +} + +// NewFakeTemplateClient returns a new fake client. +func NewFakeTemplateClient(objs ...*template.WorkflowTemplate) *Template { + f := &Template{Objs: map[string]*template.WorkflowTemplate{}} + + for i, obj := range objs { + id := obj.GetId() + + if id == "" { + obj.Id = obj.GetName() + } + + f.Objs[id] = objs[i] + } + + return f +} + +// Create creates a new Template. +func (f *Template) Create(ctx context.Context, in *template.WorkflowTemplate) error { + id := in.GetId() + + if id == "" { + id = in.GetName() + } + + if _, ok := f.Objs[id]; ok { + return errors.New("duplicate") + } + + f.Objs[id] = &template.WorkflowTemplate{ + Id: id, + Name: in.GetName(), + Data: in.GetData(), + } + + in.Id = id + + return nil +} + +// Get gets a Template from Tinkerbell. +func (f *Template) Get(ctx context.Context, id, name string) (*template.WorkflowTemplate, error) { + if id == "" { + id = name + } + + if _, ok := f.Objs[id]; ok { + return f.Objs[id], nil + } + + return nil, client.ErrNotFound +} + +// Delete deletes a Template from Tinkerbell. +func (f *Template) Delete(ctx context.Context, id, name string) error { + if id == "" { + id = name + } + + if _, ok := f.Objs[id]; ok { + delete(f.Objs, id) + + return nil + } + + return client.ErrNotFound +} + +// Update updates a Template from Tinkerbell. +func (f *Template) Update(ctx context.Context, in *template.WorkflowTemplate) error { + id := in.GetId() + + if id == "" { + in.Id = in.GetName() + } + + if _, ok := f.Objs[id]; ok { + f.Objs[id].Data = in.GetData() + + return nil + } + + return errors.New("nobody home") +} diff --git a/tink/internal/client_test/client_test.go b/tink/internal/client_test/client_test.go new file mode 100644 index 00000000..7caca671 --- /dev/null +++ b/tink/internal/client_test/client_test.go @@ -0,0 +1,133 @@ +/* +Copyright 2020 The Kubernetes 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 client_test + +import ( + "context" + "errors" + + "github.com/tinkerbell/tink/protos/template" + "google.golang.org/grpc" +) + +type fakeClient struct { + objs map[string]*template.WorkflowTemplate +} + +func newFakeClient(objs ...*template.WorkflowTemplate) *fakeClient { + f := &fakeClient{objs: map[string]*template.WorkflowTemplate{}} + + for i, obj := range objs { + id := obj.GetId() + + if id == "" { + obj.Id = obj.GetName() + } + + f.objs[id] = objs[i] + } + + return f +} + +func (f *fakeClient) CreateTemplate( + ctx context.Context, + in *template.WorkflowTemplate, + opts ...grpc.CallOption, +) (*template.CreateResponse, error) { + id := in.GetId() + + if id == "" { + id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + return nil, errors.New("duplicate") + } + + f.objs[id] = &template.WorkflowTemplate{ + Id: id, + Name: in.GetName(), + Data: in.GetData(), + } + + return &template.CreateResponse{Id: id}, nil +} + +func (f *fakeClient) GetTemplate( + ctx context.Context, + in *template.GetRequest, + opts ...grpc.CallOption, +) (*template.WorkflowTemplate, error) { + id := in.GetId() + if id == "" { + id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + return f.objs[id], nil + } + + return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") +} + +func (f *fakeClient) DeleteTemplate( + ctx context.Context, + in *template.GetRequest, + opts ...grpc.CallOption, +) (*template.Empty, error) { + id := in.GetId() + if id == "" { + id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + delete(f.objs, id) + + return &template.Empty{}, nil + } + + return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") +} + +func (f *fakeClient) ListTemplates( + ctx context.Context, + in *template.ListRequest, + opts ...grpc.CallOption, +) (template.TemplateService_ListTemplatesClient, error) { + return nil, errors.New("nobody home") +} + +func (f *fakeClient) UpdateTemplate( + ctx context.Context, + in *template.WorkflowTemplate, + opts ...grpc.CallOption, +) (*template.Empty, error) { + id := in.GetId() + + if id == "" { + in.Id = in.GetName() + } + + if _, ok := f.objs[id]; ok { + f.objs[id].Data = in.GetData() + + return &template.Empty{}, nil + } + + return nil, errors.New("nobody home") +} diff --git a/tink/internal/controllers/common/common.go b/tink/internal/controllers/common/common.go new file mode 100644 index 00000000..f1e9ef42 --- /dev/null +++ b/tink/internal/controllers/common/common.go @@ -0,0 +1,79 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package common contains common controller logic for Tinkerbell controllers. +package common + +import ( + "context" + "errors" + "fmt" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// ErrNotImplemented is returned if a requested action is not yet implemented. +var ErrNotImplemented = errors.New("not implemented") + +// Object is a temporary type introduced as a stopgap until we can update +// controller-runtime to v0.7.x+. +type Object interface { + metav1.Object + runtime.Object +} + +// EnsureFinalizer ensures the given finalizer is applied to the resource. +func EnsureFinalizer(ctx context.Context, c client.Client, logger logr.Logger, obj Object, finalizer string) error { + patch := client.MergeFrom(obj.DeepCopyObject()) + + controllerutil.AddFinalizer(obj, finalizer) + + if err := c.Patch(ctx, obj, patch); err != nil { + logger.Error(err, "Failed to add finalizer to resource") + + return fmt.Errorf("failed to add finalizer to resource: %w", err) + } + + return nil +} + +// EnsureAnnotation ensures the given annotation key and value are applied +// to the resource. +func EnsureAnnotation(ctx context.Context, c client.Client, logger logr.Logger, obj Object, key, value string) error { + patch := client.MergeFrom(obj.DeepCopyObject()) + + annotations := obj.GetAnnotations() + + if annotations == nil { + annotations = map[string]string{} + } + + annotations[key] = value + + obj.SetAnnotations(annotations) + + if err := c.Patch(ctx, obj, patch); err != nil { + logger.Error(err, "Failed to add annotation to resource") + + return fmt.Errorf("failed to add annotation to resource: %w", err) + } + + return nil +} diff --git a/tink/controllers/common_test.go b/tink/internal/controllers/common_test/common_test.go similarity index 51% rename from tink/controllers/common_test.go rename to tink/internal/controllers/common_test/common_test.go index 7d36b1d5..f55dd16f 100644 --- a/tink/controllers/common_test.go +++ b/tink/internal/controllers/common_test/common_test.go @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package controllers contains controllers for Tinkerbell. -package controllers +package common_test import ( "context" @@ -23,6 +22,7 @@ import ( . "github.com/onsi/gomega" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -func Test_ensureFinalizer(t *testing.T) { +func Test_EnsureFinalizer(t *testing.T) { g := NewWithT(t) scheme := runtime.NewScheme() @@ -38,7 +38,7 @@ func Test_ensureFinalizer(t *testing.T) { tests := []struct { name string - in Object + in common.Object finalizer string wantErr bool }{ @@ -66,22 +66,89 @@ func Test_ensureFinalizer(t *testing.T) { wantErr: false, }, } - for _, tt := range tests { + for i := range tests { + tt := tests[i] t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) + ctx := context.Background() fakeClient := fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopyObject()) - err := ensureFinalizer(context.Background(), fakeClient, log.Log, tt.in, tt.finalizer) + err := common.EnsureFinalizer(ctx, fakeClient, log.Log, tt.in, tt.finalizer) if tt.wantErr { g.Expect(err).To(HaveOccurred()) + return } g.Expect(err).NotTo(HaveOccurred()) actual := &tinkv1alpha1.Template{} key := client.ObjectKey{Name: tt.in.GetName()} - g.Expect(fakeClient.Get(context.Background(), key, actual)).To(Succeed()) + g.Expect(fakeClient.Get(ctx, key, actual)).To(Succeed()) g.Expect(actual.Finalizers).To(ContainElement(tt.finalizer)) }) } } + +func Test_EnsureAnnotation(t *testing.T) { + g := NewWithT(t) + scheme := runtime.NewScheme() + + g.Expect(tinkv1alpha1.AddToScheme(scheme)).To(Succeed()) + + tests := []struct { + name string + in common.Object + annotationKey string + annotationValue string + wantErr bool + }{ + { + name: "Adds annotation when not present", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: tinkv1alpha1.TemplateSpec{}, + }, + annotationKey: "key", + annotationValue: "value", + wantErr: false, + }, + { + name: "Annotation already exists", + in: &tinkv1alpha1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + "color": "green", + }, + }, + Spec: tinkv1alpha1.TemplateSpec{}, + }, + annotationKey: "color", + annotationValue: "blue", + wantErr: false, + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + fakeClient := fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopyObject()) + + err := common.EnsureAnnotation(ctx, fakeClient, log.Log, tt.in, tt.annotationKey, tt.annotationValue) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + + return + } + g.Expect(err).NotTo(HaveOccurred()) + + actual := &tinkv1alpha1.Template{} + key := client.ObjectKey{Name: tt.in.GetName()} + g.Expect(fakeClient.Get(ctx, key, actual)).To(Succeed()) + g.Expect(actual.Annotations).To(HaveKeyWithValue(tt.annotationKey, tt.annotationValue)) + }) + } +} diff --git a/tink/controllers/hardware_controller.go b/tink/internal/controllers/hardware/controller.go similarity index 72% rename from tink/controllers/hardware_controller.go rename to tink/internal/controllers/hardware/controller.go index c85b70c9..fb8ee58e 100644 --- a/tink/controllers/hardware_controller.go +++ b/tink/internal/controllers/hardware/controller.go @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package controllers contains controllers for Tinkerbell. -package controllers +// Package hardware contains controller for Tinkerbell Hardware. +package hardware import ( "context" @@ -23,6 +23,7 @@ import ( "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" "github.com/tinkerbell/tink/protos/hardware" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -32,8 +33,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -// HardwareReconciler implements Reconciler interface by managing Tinkerbell hardware. -type HardwareReconciler struct { +// Reconciler implements Reconciler interface by managing Tinkerbell hardware. +type Reconciler struct { client.Client HardwareClient hardware.HardwareServiceClient Log logr.Logger @@ -42,7 +43,7 @@ type HardwareReconciler struct { } // SetupWithManager configures reconciler with a given manager. -func (r *HardwareReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&tinkv1alpha1.Hardware{}). Complete(r) @@ -51,7 +52,7 @@ func (r *HardwareReconciler) SetupWithManager(mgr ctrl.Manager) error { // +kubebuilder:rbac:groups=tinkerbell.org,resources=hardware;hardware/status,verbs=get;list;watch;create;update;patch;delete // Reconcile ensures state of Tinkerbell hardware. -func (r *HardwareReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { +func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() logger := r.Log.WithValues("hardware", req.NamespacedName.Name) @@ -63,13 +64,14 @@ func (r *HardwareReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } logger.Error(err, "Failed to get hardware") - return ctrl.Result{}, fmt.Errorf("getting hardware: %w", err) + + return ctrl.Result{}, fmt.Errorf("failed to get hardware: %w", err) } // Ensure that we add the finalizer to the resource - err := ensureFinalizer(ctx, r.Client, logger, hardware, tinkv1alpha1.HardwareFinalizer) + err := common.EnsureFinalizer(ctx, r.Client, logger, hardware, tinkv1alpha1.HardwareFinalizer) if err != nil { - return ctrl.Result{}, err + return ctrl.Result{}, fmt.Errorf("failed to ensure finalizer on hardware: %w", err) } // Handle deleted hardware. @@ -80,16 +82,16 @@ func (r *HardwareReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return r.reconcileNormal(hardware) } -func (r *HardwareReconciler) reconcileNormal(hardware *tinkv1alpha1.Hardware) (ctrl.Result, error) { +func (r *Reconciler) reconcileNormal(hardware *tinkv1alpha1.Hardware) (ctrl.Result, error) { logger := r.Log.WithValues("hardware", hardware.Name) - err := ErrNotImplemented + err := common.ErrNotImplemented logger.Error(err, "Not yet implemented") return ctrl.Result{}, err } -func (r *HardwareReconciler) reconcileDelete(ctx context.Context, h *tinkv1alpha1.Hardware) (ctrl.Result, error) { +func (r *Reconciler) reconcileDelete(ctx context.Context, h *tinkv1alpha1.Hardware) (ctrl.Result, error) { // Create a patch for use later patch := client.MergeFrom(h.DeepCopy()) @@ -102,8 +104,9 @@ func (r *HardwareReconciler) reconcileDelete(ctx context.Context, h *tinkv1alpha id, ok := h.Annotations[tinkv1alpha1.HardwareIDAnnotation] if !ok { // TODO: figure out how to lookup a hardware without an ID - logger.Error(ErrNotImplemented, "Unable to delete a hardware without having recorded the ID") - return ctrl.Result{}, ErrNotImplemented + logger.Error(common.ErrNotImplemented, "Unable to delete a hardware without having recorded the ID") + + return ctrl.Result{}, common.ErrNotImplemented } hardwareRequest := &hardware.DeleteRequest{ @@ -115,13 +118,16 @@ func (r *HardwareReconciler) reconcileDelete(ctx context.Context, h *tinkv1alpha // than parsing for this specific error message. if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { logger.Error(err, "Failed to delete hardware from Tinkerbell") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to delete hardware from Tinkerbell: %w", err) } controllerutil.RemoveFinalizer(h, tinkv1alpha1.HardwareFinalizer) + if err := r.Client.Patch(ctx, h, patch); err != nil { logger.Error(err, "Failed to patch hardware") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to patch hardware: %w", err) } return ctrl.Result{}, nil diff --git a/tink/controllers/template_controller.go b/tink/internal/controllers/template/controller.go similarity index 51% rename from tink/controllers/template_controller.go rename to tink/internal/controllers/template/controller.go index 8e932730..516fae4a 100644 --- a/tink/controllers/template_controller.go +++ b/tink/internal/controllers/template/controller.go @@ -14,15 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package controllers contains controllers for Tinkerbell. -package controllers +// Package template contains controller for Tinkerbell Templates. +package template import ( "context" + "errors" "fmt" "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" "github.com/tinkerbell/tink/protos/template" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -32,18 +35,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -var ErrNotImplemented = fmt.Errorf("not implemented") +type TemplateClient interface { + Get(ctx context.Context, id, name string) (*template.WorkflowTemplate, error) + Create(ctx context.Context, template *template.WorkflowTemplate) error + Update(ctx context.Context, template *template.WorkflowTemplate) error + Delete(ctx context.Context, id, name string) error +} -// TemplateReconciler implements Reconciler interface by managing Tinkerbell templates. -type TemplateReconciler struct { +// Reconciler implements the Reconciler interface for managing Tinkerbell templates. +type Reconciler struct { client.Client - TemplateClient template.TemplateServiceClient + TemplateClient TemplateClient Log logr.Logger Scheme *runtime.Scheme } // SetupWithManager configures reconciler with a given manager. -func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&tinkv1alpha1.Template{}). Complete(r) @@ -52,7 +60,7 @@ func (r *TemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { // +kubebuilder:rbac:groups=tinkerbell.org,resources=templates;templates/status,verbs=get;list;watch;create;update;patch;delete // Reconcile ensures state of Tinkerbell templates. -func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { +func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() logger := r.Log.WithValues("template", req.NamespacedName.Name) @@ -64,13 +72,13 @@ func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } logger.Error(err, "Failed to get template") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to get template: %w", err) } // Ensure that we add the finalizer to the resource - err := ensureFinalizer(ctx, r.Client, logger, template, tinkv1alpha1.TemplateFinalizer) - if err != nil { - return ctrl.Result{}, err + if err := common.EnsureFinalizer(ctx, r.Client, logger, template, tinkv1alpha1.TemplateFinalizer); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to ensure finalizer on template: %w", err) } // Handle deleted templates. @@ -81,61 +89,41 @@ func (r *TemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return r.reconcileNormal(ctx, template) } -func (r *TemplateReconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { +func (r *Reconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { // Create a patch for use later patch := client.MergeFrom(t.DeepCopy()) logger := r.Log.WithValues("template", t.Name) - templateRequest := &template.GetRequest{} - - if t.Annotations == nil { - t.Annotations = map[string]string{} - } - - if id, ok := t.Annotations[tinkv1alpha1.TemplateIDAnnotation]; ok && id != "" { - // We've already recorded the ID, so we should prefer to use that. - templateRequest.GetBy = &template.GetRequest_Id{Id: id} - } else { - // We don't have an ID, so try to get the template by name. - // TODO: this will need to be improved for multitenancy support. - templateRequest.GetBy = &template.GetRequest_Name{Name: t.Name} - } - - tinkTemplate, err := r.TemplateClient.GetTemplate(ctx, templateRequest) - if err != nil { - // TODO: Tinkerbell should return some type of status that is easier to handle - // than parsing for this specific error message. - if err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { - logger.Error(err, "Failed to get template from Tinkerbell") - return ctrl.Result{}, err - } + tinkTemplate, err := r.getTemplate(ctx, t) - // We should create the template - tinkTemplate = &template.WorkflowTemplate{ - Name: t.Name, - Data: pointer.StringPtrDerefOr(t.Spec.Data, ""), - } - resp, err := r.TemplateClient.CreateTemplate(ctx, tinkTemplate) + switch { + case errors.Is(err, tinkclient.ErrNotFound): + result, err := r.createTemplate(ctx, t) if err != nil { - logger.Error(err, "Failed to create template in Tinkerbell") return ctrl.Result{}, err } - tinkTemplate.Id = resp.GetId() + + tinkTemplate = result + case err != nil: + return ctrl.Result{}, err } // If the data is specified and different than what is stored in Tinkerbell, // update the template in Tinkerbell if t.Spec.Data != nil && *t.Spec.Data != tinkTemplate.GetData() { tinkTemplate.Data = *t.Spec.Data - if _, err := r.TemplateClient.UpdateTemplate(ctx, tinkTemplate); err != nil { + if err := r.TemplateClient.Update(ctx, tinkTemplate); err != nil { logger.Error(err, "Failed to update template in Tinkerbell") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to update template in Tinkerbell: %w", err) } } - // Ensure that we populate the ID - t.Annotations[tinkv1alpha1.TemplateIDAnnotation] = tinkTemplate.Id + if err := common.EnsureAnnotation(ctx, r.Client, logger, t, tinkv1alpha1.TemplateIDAnnotation, + tinkTemplate.Id); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to ensure id annotation on template: %w", err) + } // If data was not specified, we are importing a pre-existing resource and should // populate it from Tinkerbell @@ -145,45 +133,66 @@ func (r *TemplateReconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha if err := r.Client.Patch(ctx, t, patch); err != nil { logger.Error(err, "Failed to patch template") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to patch template: %w", err) } return ctrl.Result{}, nil } -func (r *TemplateReconciler) reconcileDelete(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { +func (r *Reconciler) createTemplate(ctx context.Context, t *tinkv1alpha1.Template) (*template.WorkflowTemplate, error) { + logger := r.Log.WithValues("template", t.Name) + + tinkTemplate := &template.WorkflowTemplate{ + Name: t.Name, + Data: pointer.StringPtrDerefOr(t.Spec.Data, ""), + } + + if err := r.TemplateClient.Create(ctx, tinkTemplate); err != nil { + logger.Error(err, "Failed to create template in Tinkerbell") + + return nil, fmt.Errorf("failed to create template in Tinkerbell: %w", err) + } + + return tinkTemplate, nil +} + +func (r *Reconciler) getTemplate(ctx context.Context, t *tinkv1alpha1.Template) (*template.WorkflowTemplate, error) { + annotations := t.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + return r.TemplateClient.Get(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation], t.Name) +} + +func (r *Reconciler) reconcileDelete(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { // Create a patch for use later patch := client.MergeFrom(t.DeepCopy()) logger := r.Log.WithValues("template", t.Name) - if t.Annotations == nil { - t.Annotations = map[string]string{} + annotations := t.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} } - templateRequest := &template.GetRequest{} + err := r.TemplateClient.Delete(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation], t.Name) - if id, ok := t.Annotations[tinkv1alpha1.TemplateIDAnnotation]; ok && id != "" { - // We've already recorded the ID, so we should prefer to use that. - templateRequest.GetBy = &template.GetRequest_Id{Id: id} - } else { - // We don't have an ID, so try to get the template by name. - // TODO: this will need to be improved for multitenancy support. - templateRequest.GetBy = &template.GetRequest_Name{Name: t.Name} - } - - _, err := r.TemplateClient.DeleteTemplate(ctx, templateRequest) // TODO: Tinkerbell should return some type of status that is easier to handle // than parsing for this specific error message. - if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { + if err != nil && !errors.Is(err, tinkclient.ErrNotFound) { logger.Error(err, "Failed to delete template from Tinkerbell") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to delete template from Tinkerbell: %w", err) } controllerutil.RemoveFinalizer(t, tinkv1alpha1.TemplateFinalizer) + if err := r.Client.Patch(ctx, t, patch); err != nil { logger.Error(err, "Failed to patch template") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to patch template: %w", err) } return ctrl.Result{}, nil diff --git a/tink/controllers/template_controller_test.go b/tink/internal/controllers/template/controller_internal_test.go similarity index 71% rename from tink/controllers/template_controller_test.go rename to tink/internal/controllers/template/controller_internal_test.go index f946721a..004a88d9 100644 --- a/tink/controllers/template_controller_test.go +++ b/tink/internal/controllers/template/controller_internal_test.go @@ -15,17 +15,16 @@ limitations under the License. */ // Package controllers contains controllers for Tinkerbell. -package controllers +package template import ( "context" - "errors" "testing" . "github.com/onsi/gomega" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + tinkfake "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client/fake" "github.com/tinkerbell/tink/protos/template" - "google.golang.org/grpc" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/pointer" @@ -46,93 +45,6 @@ tasks: image: hello-world timeout: 60` -type fakeTemplateServiceClient struct { - objs map[string]*template.WorkflowTemplate -} - -func newFakeTemplateServiceClient(objs ...*template.WorkflowTemplate) *fakeTemplateServiceClient { - f := &fakeTemplateServiceClient{objs: map[string]*template.WorkflowTemplate{}} - - for i, obj := range objs { - id := obj.GetId() - - if id == "" { - obj.Id = obj.GetName() - } - - f.objs[id] = objs[i] - } - - return f -} - -func (f *fakeTemplateServiceClient) CreateTemplate(ctx context.Context, in *template.WorkflowTemplate, opts ...grpc.CallOption) (*template.CreateResponse, error) { - id := in.GetId() - - if id == "" { - id = in.GetName() - - } - - if _, ok := f.objs[id]; ok { - return nil, errors.New("duplicate") - } - - f.objs[id] = &template.WorkflowTemplate{ - Id: id, - Name: in.GetName(), - Data: in.GetData(), - } - - return &template.CreateResponse{Id: id}, nil -} - -func (f *fakeTemplateServiceClient) GetTemplate(ctx context.Context, in *template.GetRequest, opts ...grpc.CallOption) (*template.WorkflowTemplate, error) { - id := in.GetId() - if id == "" { - id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - return f.objs[id], nil - } - - return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") -} - -func (f *fakeTemplateServiceClient) DeleteTemplate(ctx context.Context, in *template.GetRequest, opts ...grpc.CallOption) (*template.Empty, error) { - id := in.GetId() - if id == "" { - id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - delete(f.objs, id) - return &template.Empty{}, nil - } - - return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") -} - -func (f *fakeTemplateServiceClient) ListTemplates(ctx context.Context, in *template.ListRequest, opts ...grpc.CallOption) (template.TemplateService_ListTemplatesClient, error) { - return nil, errors.New("nobody home") -} - -func (f *fakeTemplateServiceClient) UpdateTemplate(ctx context.Context, in *template.WorkflowTemplate, opts ...grpc.CallOption) (*template.Empty, error) { - id := in.GetId() - - if id == "" { - in.Id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - f.objs[id].Data = in.GetData() - return &template.Empty{}, nil - } - - return nil, errors.New("nobody home") -} - func TestTemplateReconciler_reconcileDelete(t *testing.T) { g := NewWithT(t) scheme := runtime.NewScheme() @@ -203,13 +115,14 @@ func TestTemplateReconciler_reconcileDelete(t *testing.T) { wantErr: false, }, } - for _, tt := range tests { + for i := range tests { + tt := tests[i] t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - fakeTemplateClient := newFakeTemplateServiceClient(tt.tinkObjs...) + fakeTemplateClient := tinkfake.NewFakeTemplateClient(tt.tinkObjs...) - r := &TemplateReconciler{ + r := &Reconciler{ Client: fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopy()), TemplateClient: fakeTemplateClient, Log: log.Log, @@ -219,6 +132,7 @@ func TestTemplateReconciler_reconcileDelete(t *testing.T) { got, err := r.reconcileDelete(context.Background(), tt.in) if tt.wantErr { g.Expect(err).To(HaveOccurred()) + return } g.Expect(err).NotTo(HaveOccurred()) @@ -226,13 +140,13 @@ func TestTemplateReconciler_reconcileDelete(t *testing.T) { g.Expect(got).To(BeEquivalentTo(tt.want)) // Verify that the deletion happened in the fakeTemplateServiceClient - g.Expect(fakeTemplateClient.objs).NotTo(HaveKey(tt.in.Name)) + g.Expect(fakeTemplateClient.Objs).NotTo(HaveKey(tt.in.Name)) // Check for absence of a finalizer since the fake client doesn't // do automatic deletion key := client.ObjectKey{Name: tt.in.Name} after := &tinkv1alpha1.Template{} - err = r.Client.Get(context.Background(), key, after) + g.Expect(r.Client.Get(context.Background(), key, after)).To(Succeed()) g.Expect(after.Finalizers).NotTo(ContainElement(tinkv1alpha1.TemplateFinalizer)) }) } @@ -302,13 +216,14 @@ func TestTemplateReconciler_reconcileNormal(t *testing.T) { wantErr: false, }, } - for _, tt := range tests { + for i := range tests { + tt := tests[i] t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - fakeTemplateClient := newFakeTemplateServiceClient(tt.tinkObjs...) + fakeTemplateClient := tinkfake.NewFakeTemplateClient(tt.tinkObjs...) - r := &TemplateReconciler{ + r := &Reconciler{ Client: fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopy()), TemplateClient: fakeTemplateClient, Log: log.Log, @@ -318,6 +233,7 @@ func TestTemplateReconciler_reconcileNormal(t *testing.T) { got, err := r.reconcileNormal(context.Background(), tt.in) if tt.wantErr { g.Expect(err).To(HaveOccurred()) + return } g.Expect(err).NotTo(HaveOccurred()) @@ -338,10 +254,10 @@ func TestTemplateReconciler_reconcileNormal(t *testing.T) { g.Expect(id).NotTo(BeEmpty()) // Verify that the resource exists in the fakeTemplateServiceClient - g.Expect(fakeTemplateClient.objs).To(HaveKey(id)) + g.Expect(fakeTemplateClient.Objs).To(HaveKey(id)) // get the tink resource from the fake client - tinkTemplate := fakeTemplateClient.objs[id] + tinkTemplate := fakeTemplateClient.Objs[id] // Verify the IDs match g.Expect(tinkTemplate.Id, k8sTemplate.Annotations[tinkv1alpha1.TemplateIDAnnotation]) diff --git a/tink/controllers/workflow_controller.go b/tink/internal/controllers/workflow/controller.go similarity index 84% rename from tink/controllers/workflow_controller.go rename to tink/internal/controllers/workflow/controller.go index 5d965b8c..cb84914f 100644 --- a/tink/controllers/workflow_controller.go +++ b/tink/internal/controllers/workflow/controller.go @@ -19,9 +19,11 @@ package controllers import ( "context" + "fmt" "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" "github.com/tinkerbell/tink/protos/workflow" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -62,13 +64,14 @@ func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } logger.Error(err, "Failed to get workflow") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to get workflow: %w", err) } // Ensure that we add the finalizer to the resource - err := ensureFinalizer(ctx, r.Client, logger, workflow, tinkv1alpha1.WorkflowFinalizer) + err := common.EnsureFinalizer(ctx, r.Client, logger, workflow, tinkv1alpha1.WorkflowFinalizer) if err != nil { - return ctrl.Result{}, err + return ctrl.Result{}, fmt.Errorf("failed to ensure finalizer on workflow: %w", err) } // Handle deleted wokflows. @@ -81,7 +84,7 @@ func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { func (r *WorkflowReconciler) reconcileNormal(workflow *tinkv1alpha1.Workflow) (ctrl.Result, error) { logger := r.Log.WithValues("workflow", workflow.Name) - err := ErrNotImplemented + err := common.ErrNotImplemented logger.Error(err, "Not yet implemented") @@ -101,8 +104,9 @@ func (r *WorkflowReconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha id, ok := w.Annotations[tinkv1alpha1.WorkflowIDAnnotation] if !ok { // TODO: figure out how to lookup a workflow without an ID - logger.Error(ErrNotImplemented, "Unable to delete a workflow without having recorded the ID") - return ctrl.Result{}, ErrNotImplemented + logger.Error(common.ErrNotImplemented, "Unable to delete a workflow without having recorded the ID") + + return ctrl.Result{}, common.ErrNotImplemented } workflowRequest := &workflow.GetRequest{ @@ -114,13 +118,16 @@ func (r *WorkflowReconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha // than parsing for this specific error message. if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { logger.Error(err, "Failed to delete workflow from Tinkerbell") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to delete workflow from Tinkerbell: %w", err) } controllerutil.RemoveFinalizer(w, tinkv1alpha1.WorkflowFinalizer) + if err := r.Client.Patch(ctx, w, patch); err != nil { logger.Error(err, "Failed to patch workflow") - return ctrl.Result{}, err + + return ctrl.Result{}, fmt.Errorf("failed to patch workflow: %w", err) } return ctrl.Result{}, nil From 4bffdc1b592582c84c805412bebd57a3344fbda0 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 16 Dec 2020 09:53:24 -0500 Subject: [PATCH 07/15] current state --- tink/internal/client/common.go | 29 +++++ tink/internal/client/fake/hardware.go | 62 +++++++++ .../client/fake/{client.go => template.go} | 6 +- tink/internal/client/fake/workflow.go | 76 +++++++++++ tink/internal/client/hardware.go | 78 ++++++++++++ .../client/{client.go => template.go} | 30 ++--- tink/internal/client/workflow.go | 76 +++++++++++ tink/internal/client_test/common_test.go | 89 +++++++++++++ tink/internal/client_test/template_test.go | 119 ++++++++++++++++++ tink/internal/client_test/workflow_test.go | 54 ++++++++ 10 files changed, 595 insertions(+), 24 deletions(-) create mode 100644 tink/internal/client/common.go create mode 100644 tink/internal/client/fake/hardware.go rename tink/internal/client/fake/{client.go => template.go} (95%) create mode 100644 tink/internal/client/fake/workflow.go create mode 100644 tink/internal/client/hardware.go rename tink/internal/client/{client.go => template.go} (71%) create mode 100644 tink/internal/client/workflow.go create mode 100644 tink/internal/client_test/common_test.go create mode 100644 tink/internal/client_test/template_test.go create mode 100644 tink/internal/client_test/workflow_test.go diff --git a/tink/internal/client/common.go b/tink/internal/client/common.go new file mode 100644 index 00000000..182a42ee --- /dev/null +++ b/tink/internal/client/common.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Kubernetes 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 client contains a client wrapper for Tinkerbell. +package client + +import ( + "errors" +) + +// ErrNotFound is returned if a requested resource is not found. +var ErrNotFound = errors.New("resource not found") + +// TODO: Tinkerbell should return some type of status that is easier to handle +// than parsing for this specific error message. +const sqlErrorString = "rpc error: code = Unknown desc = sql: no rows in result set" diff --git a/tink/internal/client/fake/hardware.go b/tink/internal/client/fake/hardware.go new file mode 100644 index 00000000..c5ce2f91 --- /dev/null +++ b/tink/internal/client/fake/hardware.go @@ -0,0 +1,62 @@ +/* +Copyright 2020 The Kubernetes 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 fake contains a fake client wrapper for Tinkerbell. +package fake + +import ( + "context" + + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/tink/protos/hardware" +) + +// Hardware is a fake client for Tinkerbell Hardwares. +type Hardware struct { + Objs map[string]*hardware.Hardware +} + +// NewFakeHardwareClient returns a new fake client. +func NewFakeHardwareClient(objs ...*hardware.Hardware) *Hardware { + f := &Hardware{Objs: map[string]*hardware.Hardware{}} + + for i, obj := range objs { + f.Objs[obj.GetId()] = objs[i] + } + + return f +} + +// Get gets a Hardware from Tinkerbell. +func (f *Hardware) Get(ctx context.Context, id, mac, ip string) (*hardware.Hardware, error) { + // TODO: need to implement fake ip and mac lookups + if _, ok := f.Objs[id]; ok { + return f.Objs[id], nil + } + + return nil, client.ErrNotFound +} + +// Delete deletes a Hardware from Tinkerbell. +func (f *Hardware) Delete(ctx context.Context, id string) error { + if _, ok := f.Objs[id]; ok { + delete(f.Objs, id) + + return nil + } + + return client.ErrNotFound +} diff --git a/tink/internal/client/fake/client.go b/tink/internal/client/fake/template.go similarity index 95% rename from tink/internal/client/fake/client.go rename to tink/internal/client/fake/template.go index 61a90806..8d887b17 100644 --- a/tink/internal/client/fake/client.go +++ b/tink/internal/client/fake/template.go @@ -84,11 +84,7 @@ func (f *Template) Get(ctx context.Context, id, name string) (*template.Workflow } // Delete deletes a Template from Tinkerbell. -func (f *Template) Delete(ctx context.Context, id, name string) error { - if id == "" { - id = name - } - +func (f *Template) Delete(ctx context.Context, id string) error { if _, ok := f.Objs[id]; ok { delete(f.Objs, id) diff --git a/tink/internal/client/fake/workflow.go b/tink/internal/client/fake/workflow.go new file mode 100644 index 00000000..935ebfad --- /dev/null +++ b/tink/internal/client/fake/workflow.go @@ -0,0 +1,76 @@ +/* +Copyright 2020 The Kubernetes 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 fake contains a fake client wrapper for Tinkerbell. +package fake + +import ( + "context" + + "github.com/google/uuid" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/tink/protos/workflow" +) + +// Workflow is a fake client for Tinkerbell Workflows. +type Workflow struct { + Objs map[string]*workflow.Workflow +} + +// NewFakeWorkflowClient returns a new fake client. +func NewFakeWorkflowClient(objs ...*workflow.Workflow) *Workflow { + f := &Workflow{Objs: map[string]*workflow.Workflow{}} + + for i, obj := range objs { + f.Objs[obj.GetId()] = objs[i] + } + + return f +} + +// Create creates a new Workflow. +func (f *Workflow) Create(ctx context.Context, template, hardware string) (string, error) { + id := uuid.New().String() + + f.Objs[id] = &workflow.Workflow{ + Id: id, + Template: template, + Hardware: hardware, + // TODO: populate fake Data + } + + return id, nil +} + +// Get gets a Workflow from Tinkerbell. +func (f *Workflow) Get(ctx context.Context, id string) (*workflow.Workflow, error) { + if _, ok := f.Objs[id]; ok { + return f.Objs[id], nil + } + + return nil, client.ErrNotFound +} + +// Delete deletes a Workflow from Tinkerbell. +func (f *Workflow) Delete(ctx context.Context, id string) error { + if _, ok := f.Objs[id]; ok { + delete(f.Objs, id) + + return nil + } + + return client.ErrNotFound +} diff --git a/tink/internal/client/hardware.go b/tink/internal/client/hardware.go new file mode 100644 index 00000000..3c8be5a2 --- /dev/null +++ b/tink/internal/client/hardware.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 The Kubernetes 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 client + +import ( + "context" + "fmt" + + "github.com/tinkerbell/tink/protos/hardware" + "google.golang.org/grpc" +) + +// Hardware client for Tinkerbell. +type Hardware struct { + client hardware.HardwareServiceClient +} + +// NewHardwareClient returns a Hardware client. +func NewHardwareClient(client hardware.HardwareServiceClient) Hardware { + return Hardware{client: client} +} + +// Get returns a Tinkerbell Hardware. +func (t *Hardware) Get(ctx context.Context, id, ip, mac string) (*hardware.Hardware, error) { + var method func(context.Context, *hardware.GetRequest, ...grpc.CallOption) (*hardware.Hardware, error) + + req := &hardware.GetRequest{} + + switch { + case id != "": + req.Id = id + method = t.client.ByID + case mac != "": + req.Mac = mac + method = t.client.ByMAC + case ip != "": + req.Ip = ip + method = t.client.ByIP + } + + tinkHardware, err := method(ctx, req) + if err != nil { + if err.Error() == sqlErrorString { + return nil, fmt.Errorf("hardware %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get hardware from Tinkerbell: %w", err) + } + + return tinkHardware, nil +} + +// Delete a Tinkerbell Hardware. +func (t *Hardware) Delete(ctx context.Context, id string) error { + if _, err := t.client.Delete(ctx, &hardware.DeleteRequest{Id: id}); err != nil { + if err.Error() == sqlErrorString { + return fmt.Errorf("hardware %w", ErrNotFound) + } + + return fmt.Errorf("failed to delete hardware from Tinkerbell: %w", err) + } + + return nil +} diff --git a/tink/internal/client/client.go b/tink/internal/client/template.go similarity index 71% rename from tink/internal/client/client.go rename to tink/internal/client/template.go index 2bcb71fc..d4ad33d0 100644 --- a/tink/internal/client/client.go +++ b/tink/internal/client/template.go @@ -14,29 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package client contains a client wrapper for Tinkerbell. package client import ( "context" - "errors" "fmt" "github.com/tinkerbell/tink/protos/template" ) -// ErrNotFound is returned if a requested resource is not found. -var ErrNotFound = errors.New("resource not found") - // Template client for Tinkerbell. type Template struct { client template.TemplateServiceClient } +// NewTemplateClient returns a Template client. +func NewTemplateClient(client template.TemplateServiceClient) Template { + return Template{client: client} +} + // Get returns a Tinkerbell Template. func (t *Template) Get(ctx context.Context, id, name string) (*template.WorkflowTemplate, error) { req := &template.GetRequest{} - if id == "" { + if id != "" { req.GetBy = &template.GetRequest_Id{Id: id} } else { req.GetBy = &template.GetRequest_Name{Name: name} @@ -44,9 +44,7 @@ func (t *Template) Get(ctx context.Context, id, name string) (*template.Workflow tinkTemplate, err := t.client.GetTemplate(ctx, req) if err != nil { - // TODO: Tinkerbell should return some type of status that is easier to handle - // than parsing for this specific error message. - if err.Error() == "rpc error: code = Unknown desc = sql: no rows in result set" { + if err.Error() == sqlErrorString { return nil, fmt.Errorf("template %w", ErrNotFound) } @@ -78,18 +76,12 @@ func (t *Template) Create(ctx context.Context, template *template.WorkflowTempla } // Delete a Tinkerbell Template. -func (t *Template) Delete(ctx context.Context, id, name string) error { - req := &template.GetRequest{} - if id == "" { - req.GetBy = &template.GetRequest_Id{Id: id} - } else { - req.GetBy = &template.GetRequest_Name{Name: name} +func (t *Template) Delete(ctx context.Context, id string) error { + req := &template.GetRequest{ + GetBy: &template.GetRequest_Id{Id: id}, } - if _, err := t.client.DeleteTemplate(ctx, req); err != nil { - // TODO: Tinkerbell should return some type of status that is easier to handle - // than parsing for this specific error message. - if err.Error() == "rpc error: code = Unknown desc = sql: no rows in result set" { + if err.Error() == sqlErrorString { return fmt.Errorf("template %w", ErrNotFound) } diff --git a/tink/internal/client/workflow.go b/tink/internal/client/workflow.go new file mode 100644 index 00000000..6484b1f6 --- /dev/null +++ b/tink/internal/client/workflow.go @@ -0,0 +1,76 @@ +/* +Copyright 2020 The Kubernetes 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 client + +import ( + "context" + "fmt" + + "github.com/tinkerbell/tink/protos/workflow" +) + +// Workflow client for Tinkerbell. +type Workflow struct { + client workflow.WorkflowServiceClient +} + +// NewWorkflowClient returns a Workflow client. +func NewWorkflowClient(client workflow.WorkflowServiceClient) Workflow { + return Workflow{client: client} +} + +// Get returns a Tinkerbell Workflow. +func (t *Workflow) Get(ctx context.Context, id string) (*workflow.Workflow, error) { + tinkWorkflow, err := t.client.GetWorkflow(ctx, &workflow.GetRequest{Id: id}) + if err != nil { + if err.Error() == sqlErrorString { + return nil, fmt.Errorf("workflow %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get workflow from Tinkerbell: %w", err) + } + + return tinkWorkflow, nil +} + +// Create a Tinkerbell Workflow. +func (t *Workflow) Create(ctx context.Context, template, hardware string) (string, error) { + req := &workflow.CreateRequest{ + Template: template, + Hardware: hardware, + } + + resp, err := t.client.CreateWorkflow(ctx, req) + if err != nil { + return "", fmt.Errorf("failed to create workflow in Tinkerbell: %w", err) + } + + return resp.GetId(), nil +} + +// Delete a Tinkerbell Workflow. +func (t *Workflow) Delete(ctx context.Context, id string) error { + if _, err := t.client.DeleteWorkflow(ctx, &workflow.GetRequest{Id: id}); err != nil { + if err.Error() == sqlErrorString { + return fmt.Errorf("workflow %w", ErrNotFound) + } + + return fmt.Errorf("failed to delete workflow from Tinkerbell: %w", err) + } + + return nil +} diff --git a/tink/internal/client_test/common_test.go b/tink/internal/client_test/common_test.go new file mode 100644 index 00000000..17f8ac9d --- /dev/null +++ b/tink/internal/client_test/common_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2020 The Kubernetes 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 client_test + +import ( + "crypto/x509" + "io/ioutil" + "net/http" + "os" + "testing" + + . "github.com/onsi/gomega" + "github.com/tinkerbell/tink/protos/hardware" + "github.com/tinkerbell/tink/protos/template" + "github.com/tinkerbell/tink/protos/workflow" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func generateTemplate(name, data string) *template.WorkflowTemplate { + return &template.WorkflowTemplate{ + Name: name, + Data: data, + } +} + +func realConn(t *testing.T) *grpc.ClientConn { + g := NewWithT(t) + + certURL, ok := os.LookupEnv("TINKERBELL_CERT_URL") + if !ok || certURL == "" { + t.Skip("Skipping live client tests because TINKERBELL_CERT_URL is not set.") + } + + grpcAuthority, ok := os.LookupEnv("TINKERBELL_GRPC_AUTHORITY") + if !ok || grpcAuthority == "" { + t.Skip("Skipping live client tests because TINKERBELL_GRPC_AUTHORITY is not set.") + } + + resp, err := http.Get(certURL) //nolint:noctx + g.Expect(err).NotTo(HaveOccurred()) + + defer resp.Body.Close() //nolint:errcheck + + certs, err := ioutil.ReadAll(resp.Body) + g.Expect(err).NotTo(HaveOccurred()) + + cp := x509.NewCertPool() + ok = cp.AppendCertsFromPEM(certs) + g.Expect(ok).To(BeTrue()) + + creds := credentials.NewClientTLSFromCert(cp, "tink-server") + conn, err := grpc.Dial(grpcAuthority, grpc.WithTransportCredentials(creds)) + g.Expect(err).NotTo(HaveOccurred()) + + return conn +} + +func realTemplateClient(t *testing.T) template.TemplateServiceClient { + conn := realConn(t) + + return template.NewTemplateServiceClient(conn) +} + +func realWorkflowClient(t *testing.T) workflow.WorkflowServiceClient { + conn := realConn(t) + + return workflow.NewWorkflowServiceClient(conn) +} + +func realHardwareClient(t *testing.T) hardware.HardwareServiceClient { + conn := realConn(t) + + return hardware.NewHardwareServiceClient(conn) +} diff --git a/tink/internal/client_test/template_test.go b/tink/internal/client_test/template_test.go new file mode 100644 index 00000000..2454f0fa --- /dev/null +++ b/tink/internal/client_test/template_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2020 The Kubernetes 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 client_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "k8s.io/apimachinery/pkg/util/rand" +) + +const ( + helloWorldTemplate = `version: "0.1" +name: hello_world_workflow +global_timeout: 600 +tasks: + - name: "hello world" + worker: "{{.device_1}}" + actions: + - name: "hello_world" + image: hello-world + timeout: 60` + sampleTemplate = ` +version: '0.1' +name: sample_workflow +global_timeout: 600 +tasks: + - name: "hello world first" + worker: "{{.device_1}}" + actions: + - name: "hello_world_first" + image: hello-world + timeout: 60 + - name: "hello world second" + worker: "{{.device_1}}" + actions: + - name: "hello_world_second" + image: hello-world + timeout: 60` +) + +func TestTemplateLifecycle(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + templateClient := client.NewTemplateClient(realTemplateClient(t)) + name := rand.String(12) + + // Ensure that we now get a NotFound error trying to get the template + _, err := templateClient.Get(ctx, "", name) + g.Expect(err).To(MatchError(client.ErrNotFound)) + + // Create a template using the hello world template + testTemplate := generateTemplate(name, helloWorldTemplate) + g.Expect(templateClient.Create(ctx, testTemplate)).To(Succeed()) + + // Ensure that the template now has an ID set + g.Expect(testTemplate.Id).NotTo(BeEmpty()) + expectedID := testTemplate.Id + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we can delete the template by ID + g.Expect(templateClient.Delete(ctx, expectedID)) + + // Ensure that we now get a NotFound error trying to get the template + _, err := templateClient.Get(ctx, expectedID, "") + g.Expect(err).To(MatchError(client.ErrNotFound)) + }() + + // Verify that trying to create a template with a duplicate name fails + testDuplicateName := generateTemplate(name, sampleTemplate) + g.Expect(templateClient.Create(ctx, testDuplicateName)).NotTo(Succeed()) + + // Ensure we can get the template we just created by ID + // and it has the values we expect + resByID, err := templateClient.Get(ctx, expectedID, "") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(resByID).NotTo(BeNil()) + g.Expect(resByID.Id).To(BeEquivalentTo(expectedID)) + g.Expect(resByID.Name).To(BeEquivalentTo(name)) + g.Expect(resByID.Data).To(BeEquivalentTo(helloWorldTemplate)) + + // Ensure we can get the previously created template by Name + // and it has the values we expect + resByName, err := templateClient.Get(ctx, "", testTemplate.Name) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(resByName).NotTo(BeNil()) + g.Expect(resByName.Id).To(BeEquivalentTo(expectedID)) + g.Expect(resByName.Name).To(BeEquivalentTo(name)) + g.Expect(resByName.Data).To(BeEquivalentTo(helloWorldTemplate)) + + // Update the template's data + testTemplate.Data = sampleTemplate + g.Expect(templateClient.Update(ctx, testTemplate)).To(Succeed()) + + // Esnure that the template was updated in Tinkerbell + res, err := templateClient.Get(ctx, expectedID, "") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).NotTo(BeNil()) + g.Expect(res.Id).To(BeEquivalentTo(expectedID)) + g.Expect(res.Name).To(BeEquivalentTo(name)) + g.Expect(res.Data).To(BeEquivalentTo(sampleTemplate)) +} diff --git a/tink/internal/client_test/workflow_test.go b/tink/internal/client_test/workflow_test.go new file mode 100644 index 00000000..60136a0b --- /dev/null +++ b/tink/internal/client_test/workflow_test.go @@ -0,0 +1,54 @@ +/* +Copyright 2020 The Kubernetes 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 client_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "k8s.io/apimachinery/pkg/util/rand" +) + +func TestWorkflowLifecycle(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + templateClient := client.NewTemplateClient(realTemplateClient(t)) + _ = realWorkflowClient(t) + + // Create a template for the workflow to use + testTemplate := generateTemplate(rand.String(12), helloWorldTemplate) + g.Expect(templateClient.Create(ctx, testTemplate)).To(Succeed()) + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we can delete the template by ID + g.Expect(templateClient.Delete(ctx, testTemplate.Id)) + }() + + // TODO: Create hardware for the workflow to use + + // TODO: Create the workflow + + // TODO: Ensure we can get the workflow we just created by ID + // and that it has the values we expect + + // TODO: Delete the workflow + t.Error("not ready yet") +} From fe98699ba65afb2b069a59c927ef4df8b639bd4f Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Thu, 17 Dec 2020 17:44:05 -0500 Subject: [PATCH 08/15] latest changes --- Makefile | 2 +- config/crd/bases/tinkerbell.org_hardware.yaml | 7 + go.mod | 3 + tink/api/v1alpha1/hardware_types.go | 11 +- tink/internal/client/common.go | 3 +- tink/internal/client/fake/hardware.go | 62 ++++++- tink/internal/client/fake/template.go | 55 +++--- tink/internal/client/fake/workflow.go | 29 +++- tink/internal/client/hardware.go | 32 +++- tink/internal/client/template.go | 4 +- tink/internal/client/workflow.go | 49 +++++- tink/internal/client_test/client_test.go | 133 --------------- tink/internal/client_test/common_test.go | 159 ++++++++++++++++++ tink/internal/client_test/hardware_test.go | 123 ++++++++++++++ tink/internal/client_test/template_test.go | 10 -- tink/internal/client_test/workflow_test.go | 37 +++- .../controllers/hardware/controller.go | 73 ++------ .../controllers/template/controller.go | 20 +-- .../template/controller_internal_test.go | 18 -- .../controllers/workflow/controller.go | 119 +++++++++---- 20 files changed, 615 insertions(+), 334 deletions(-) delete mode 100644 tink/internal/client_test/client_test.go create mode 100644 tink/internal/client_test/hardware_test.go diff --git a/Makefile b/Makefile index a3476aed..06f27f09 100644 --- a/Makefile +++ b/Makefile @@ -294,7 +294,7 @@ generate-manifests: $(CONTROLLER_GEN) # Generate manifests e.g. CRD, RBAC etc. paths=./api/... \ paths=./controllers/... \ paths=./tink/api/... \ - paths=./tink/controllers/... \ + paths=./tink/internal/controllers/... \ crd:crdVersions=v1 \ rbac:roleName=manager-role \ output:crd:dir=./config/crd/bases \ diff --git a/config/crd/bases/tinkerbell.org_hardware.yaml b/config/crd/bases/tinkerbell.org_hardware.yaml index a32def9d..752a01d7 100644 --- a/config/crd/bases/tinkerbell.org_hardware.yaml +++ b/config/crd/bases/tinkerbell.org_hardware.yaml @@ -33,6 +33,13 @@ spec: type: object spec: description: HardwareSpec defines the desired state of Hardware. + properties: + id: + description: ID is the ID of the hardware in Tinkerbell + minLength: 1 + type: string + required: + - id type: object status: description: HardwareStatus defines the observed state of Hardware. diff --git a/go.mod b/go.mod index 87cccb34..b6dfd5db 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,11 @@ go 1.15 require ( github.com/go-logr/logr v0.1.0 + github.com/google/uuid v1.1.2 github.com/onsi/gomega v1.10.1 github.com/tinkerbell/tink v0.0.0-20201210163923-6d9159b63857 + google.golang.org/grpc v1.32.0 + google.golang.org/protobuf v1.25.0 k8s.io/api v0.17.16 k8s.io/apimachinery v0.17.16 k8s.io/client-go v0.17.16 diff --git a/tink/api/v1alpha1/hardware_types.go b/tink/api/v1alpha1/hardware_types.go index 6fd447f4..a0517317 100644 --- a/tink/api/v1alpha1/hardware_types.go +++ b/tink/api/v1alpha1/hardware_types.go @@ -20,16 +20,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -const ( - // HardwareIDAnnotation is used by the controller to store the - // ID assigned to the hardware by Tinkerbell. - HardwareIDAnnotation = "hardware.tinkerbell.org/id" - - HardwareFinalizer = "hardware.tinkerbell.org" -) - // HardwareSpec defines the desired state of Hardware. type HardwareSpec struct { + // ID is the ID of the hardware in Tinkerbell + // +kubebuilder:validation:MinLength=1 + ID string `json:"id"` } // HardwareStatus defines the observed state of Hardware. diff --git a/tink/internal/client/common.go b/tink/internal/client/common.go index 182a42ee..81437764 100644 --- a/tink/internal/client/common.go +++ b/tink/internal/client/common.go @@ -25,5 +25,6 @@ import ( var ErrNotFound = errors.New("resource not found") // TODO: Tinkerbell should return some type of status that is easier to handle -// than parsing for this specific error message. +// than parsing for these specific error message. const sqlErrorString = "rpc error: code = Unknown desc = sql: no rows in result set" +const sqlErrorStringAlt = "rpc error: code = Unknown desc = SELECT: sql: no rows in result set" diff --git a/tink/internal/client/fake/hardware.go b/tink/internal/client/fake/hardware.go index c5ce2f91..b2581e6d 100644 --- a/tink/internal/client/fake/hardware.go +++ b/tink/internal/client/fake/hardware.go @@ -19,9 +19,12 @@ package fake import ( "context" + "errors" + "github.com/google/uuid" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" "github.com/tinkerbell/tink/protos/hardware" + "google.golang.org/protobuf/proto" ) // Hardware is a fake client for Tinkerbell Hardwares. @@ -33,18 +36,65 @@ type Hardware struct { func NewFakeHardwareClient(objs ...*hardware.Hardware) *Hardware { f := &Hardware{Objs: map[string]*hardware.Hardware{}} - for i, obj := range objs { - f.Objs[obj.GetId()] = objs[i] + for _, obj := range objs { + if obj.GetId() == "" { + obj.Id = uuid.New().String() + } + + f.Objs[obj.Id] = proto.Clone(obj).(*hardware.Hardware) } return f } +// Create creates a new Hardware. +func (f *Hardware) Create(ctx context.Context, in *hardware.Hardware) error { + if in.GetId() == "" { + in.Id = uuid.New().String() + } + + if _, ok := f.Objs[in.Id]; ok { + return errors.New("duplicate") + } + + f.Objs[in.Id] = proto.Clone(in).(*hardware.Hardware) + + return nil +} + +// Update Hardware in Tinkerbell. +func (f *Hardware) Update(ctx context.Context, in *hardware.Hardware) error { + if _, ok := f.Objs[in.Id]; ok { + f.Objs[in.Id] = proto.Clone(in).(*hardware.Hardware) + + return nil + } + + return errors.New("nobody home") +} + // Get gets a Hardware from Tinkerbell. -func (f *Hardware) Get(ctx context.Context, id, mac, ip string) (*hardware.Hardware, error) { - // TODO: need to implement fake ip and mac lookups - if _, ok := f.Objs[id]; ok { - return f.Objs[id], nil +func (f *Hardware) Get(ctx context.Context, id, ip, mac string) (*hardware.Hardware, error) { + switch { + case id != "": + if _, ok := f.Objs[id]; ok { + return proto.Clone(f.Objs[id]).(*hardware.Hardware), nil + } + default: + for _, hw := range f.Objs { + for _, i := range hw.GetNetwork().GetInterfaces() { + switch { + case mac != "": + if i.GetDhcp().GetMac() == mac { + return proto.Clone(hw).(*hardware.Hardware), nil + } + case ip != "": + if i.GetDhcp().GetIp().Address == ip { + return proto.Clone(hw).(*hardware.Hardware), nil + } + } + } + } } return nil, client.ErrNotFound diff --git a/tink/internal/client/fake/template.go b/tink/internal/client/fake/template.go index 8d887b17..c881d4ab 100644 --- a/tink/internal/client/fake/template.go +++ b/tink/internal/client/fake/template.go @@ -21,8 +21,10 @@ import ( "context" "errors" + "github.com/google/uuid" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" "github.com/tinkerbell/tink/protos/template" + "google.golang.org/protobuf/proto" ) // Template is a fake client for Tinkerbell Templates. @@ -34,14 +36,12 @@ type Template struct { func NewFakeTemplateClient(objs ...*template.WorkflowTemplate) *Template { f := &Template{Objs: map[string]*template.WorkflowTemplate{}} - for i, obj := range objs { - id := obj.GetId() - - if id == "" { - obj.Id = obj.GetName() + for _, obj := range objs { + if obj.GetId() == "" { + obj.Id = uuid.New().String() } - f.Objs[id] = objs[i] + f.Objs[obj.Id] = proto.Clone(obj).(*template.WorkflowTemplate) } return f @@ -49,35 +49,32 @@ func NewFakeTemplateClient(objs ...*template.WorkflowTemplate) *Template { // Create creates a new Template. func (f *Template) Create(ctx context.Context, in *template.WorkflowTemplate) error { - id := in.GetId() - - if id == "" { - id = in.GetName() + if in.GetId() == "" { + in.Id = uuid.New().String() } - if _, ok := f.Objs[id]; ok { + if _, ok := f.Objs[in.Id]; ok { return errors.New("duplicate") } - f.Objs[id] = &template.WorkflowTemplate{ - Id: id, - Name: in.GetName(), - Data: in.GetData(), - } - - in.Id = id + f.Objs[in.Id] = proto.Clone(in).(*template.WorkflowTemplate) return nil } // Get gets a Template from Tinkerbell. func (f *Template) Get(ctx context.Context, id, name string) (*template.WorkflowTemplate, error) { - if id == "" { - id = name - } - - if _, ok := f.Objs[id]; ok { - return f.Objs[id], nil + switch { + case id != "": + if _, ok := f.Objs[id]; ok { + return proto.Clone(f.Objs[id]).(*template.WorkflowTemplate), nil + } + default: + for _, obj := range f.Objs { + if obj.GetName() == name { + return proto.Clone(obj).(*template.WorkflowTemplate), nil + } + } } return nil, client.ErrNotFound @@ -96,14 +93,8 @@ func (f *Template) Delete(ctx context.Context, id string) error { // Update updates a Template from Tinkerbell. func (f *Template) Update(ctx context.Context, in *template.WorkflowTemplate) error { - id := in.GetId() - - if id == "" { - in.Id = in.GetName() - } - - if _, ok := f.Objs[id]; ok { - f.Objs[id].Data = in.GetData() + if _, ok := f.Objs[in.Id]; ok { + f.Objs[in.Id] = proto.Clone(in).(*template.WorkflowTemplate) return nil } diff --git a/tink/internal/client/fake/workflow.go b/tink/internal/client/fake/workflow.go index 935ebfad..dcde19a0 100644 --- a/tink/internal/client/fake/workflow.go +++ b/tink/internal/client/fake/workflow.go @@ -23,32 +23,43 @@ import ( "github.com/google/uuid" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" "github.com/tinkerbell/tink/protos/workflow" + "google.golang.org/protobuf/proto" ) // Workflow is a fake client for Tinkerbell Workflows. type Workflow struct { - Objs map[string]*workflow.Workflow + Objs map[string]*workflow.Workflow + hwClient Hardware + templateClient Template } // NewFakeWorkflowClient returns a new fake client. -func NewFakeWorkflowClient(objs ...*workflow.Workflow) *Workflow { - f := &Workflow{Objs: map[string]*workflow.Workflow{}} +func NewFakeWorkflowClient(hwClient Hardware, templateClient Template, objs ...*workflow.Workflow) *Workflow { + f := &Workflow{ + Objs: map[string]*workflow.Workflow{}, + hwClient: hwClient, + templateClient: templateClient, + } + + for _, obj := range objs { + if obj.GetId() == "" { + obj.Id = uuid.New().String() + } - for i, obj := range objs { - f.Objs[obj.GetId()] = objs[i] + f.Objs[obj.Id] = proto.Clone(obj).(*workflow.Workflow) } return f } // Create creates a new Workflow. -func (f *Workflow) Create(ctx context.Context, template, hardware string) (string, error) { +func (f *Workflow) Create(ctx context.Context, templateID, hardwareID string) (string, error) { id := uuid.New().String() f.Objs[id] = &workflow.Workflow{ Id: id, - Template: template, - Hardware: hardware, + Template: templateID, + Hardware: hardwareID, // TODO: populate fake Data } @@ -58,7 +69,7 @@ func (f *Workflow) Create(ctx context.Context, template, hardware string) (strin // Get gets a Workflow from Tinkerbell. func (f *Workflow) Get(ctx context.Context, id string) (*workflow.Workflow, error) { if _, ok := f.Objs[id]; ok { - return f.Objs[id], nil + return proto.Clone(f.Objs[id]).(*workflow.Workflow), nil } return nil, client.ErrNotFound diff --git a/tink/internal/client/hardware.go b/tink/internal/client/hardware.go index 3c8be5a2..976a3724 100644 --- a/tink/internal/client/hardware.go +++ b/tink/internal/client/hardware.go @@ -18,8 +18,10 @@ package client import ( "context" + "errors" "fmt" + "github.com/google/uuid" "github.com/tinkerbell/tink/protos/hardware" "google.golang.org/grpc" ) @@ -34,6 +36,32 @@ func NewHardwareClient(client hardware.HardwareServiceClient) Hardware { return Hardware{client: client} } +// Create Tinkerbell Hardware. +func (t *Hardware) Create(ctx context.Context, h *hardware.Hardware) error { + if h == nil { + return errors.New("hardware should not be nil") + } + + if h.GetId() == "" { + h.Id = uuid.New().String() + } + + if _, err := t.client.Push(ctx, &hardware.PushRequest{Data: h}); err != nil { + return fmt.Errorf("failed to create hardware in Tinkerbell: %w", err) + } + + return nil +} + +// Update Tinkerbell Hardware. +func (t *Hardware) Update(ctx context.Context, h *hardware.Hardware) error { + if _, err := t.client.Push(ctx, &hardware.PushRequest{Data: h}); err != nil { + return fmt.Errorf("failed to update template in Tinkerbell: %w", err) + } + + return nil +} + // Get returns a Tinkerbell Hardware. func (t *Hardware) Get(ctx context.Context, id, ip, mac string) (*hardware.Hardware, error) { var method func(context.Context, *hardware.GetRequest, ...grpc.CallOption) (*hardware.Hardware, error) @@ -54,7 +82,7 @@ func (t *Hardware) Get(ctx context.Context, id, ip, mac string) (*hardware.Hardw tinkHardware, err := method(ctx, req) if err != nil { - if err.Error() == sqlErrorString { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { return nil, fmt.Errorf("hardware %w", ErrNotFound) } @@ -67,7 +95,7 @@ func (t *Hardware) Get(ctx context.Context, id, ip, mac string) (*hardware.Hardw // Delete a Tinkerbell Hardware. func (t *Hardware) Delete(ctx context.Context, id string) error { if _, err := t.client.Delete(ctx, &hardware.DeleteRequest{Id: id}); err != nil { - if err.Error() == sqlErrorString { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { return fmt.Errorf("hardware %w", ErrNotFound) } diff --git a/tink/internal/client/template.go b/tink/internal/client/template.go index d4ad33d0..abc84098 100644 --- a/tink/internal/client/template.go +++ b/tink/internal/client/template.go @@ -44,7 +44,7 @@ func (t *Template) Get(ctx context.Context, id, name string) (*template.Workflow tinkTemplate, err := t.client.GetTemplate(ctx, req) if err != nil { - if err.Error() == sqlErrorString { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { return nil, fmt.Errorf("template %w", ErrNotFound) } @@ -81,7 +81,7 @@ func (t *Template) Delete(ctx context.Context, id string) error { GetBy: &template.GetRequest_Id{Id: id}, } if _, err := t.client.DeleteTemplate(ctx, req); err != nil { - if err.Error() == sqlErrorString { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { return fmt.Errorf("template %w", ErrNotFound) } diff --git a/tink/internal/client/workflow.go b/tink/internal/client/workflow.go index 6484b1f6..85db8b23 100644 --- a/tink/internal/client/workflow.go +++ b/tink/internal/client/workflow.go @@ -18,26 +18,29 @@ package client import ( "context" + "encoding/json" "fmt" + "github.com/tinkerbell/tink/protos/hardware" "github.com/tinkerbell/tink/protos/workflow" ) // Workflow client for Tinkerbell. type Workflow struct { - client workflow.WorkflowServiceClient + client workflow.WorkflowServiceClient + hardwareClient Hardware } // NewWorkflowClient returns a Workflow client. -func NewWorkflowClient(client workflow.WorkflowServiceClient) Workflow { - return Workflow{client: client} +func NewWorkflowClient(client workflow.WorkflowServiceClient, hClient Hardware) Workflow { + return Workflow{client: client, hardwareClient: hClient} } // Get returns a Tinkerbell Workflow. func (t *Workflow) Get(ctx context.Context, id string) (*workflow.Workflow, error) { tinkWorkflow, err := t.client.GetWorkflow(ctx, &workflow.GetRequest{Id: id}) if err != nil { - if err.Error() == sqlErrorString { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { return nil, fmt.Errorf("workflow %w", ErrNotFound) } @@ -48,10 +51,20 @@ func (t *Workflow) Get(ctx context.Context, id string) (*workflow.Workflow, erro } // Create a Tinkerbell Workflow. -func (t *Workflow) Create(ctx context.Context, template, hardware string) (string, error) { +func (t *Workflow) Create(ctx context.Context, templateID, hardwareID string) (string, error) { + h, err := t.hardwareClient.Get(ctx, hardwareID, "", "") + if err != nil { + return "", err + } + + hardwareString, err := HardwareToJSON(h) + if err != nil { + return "", err + } + req := &workflow.CreateRequest{ - Template: template, - Hardware: hardware, + Template: templateID, + Hardware: hardwareString, } resp, err := t.client.CreateWorkflow(ctx, req) @@ -65,7 +78,7 @@ func (t *Workflow) Create(ctx context.Context, template, hardware string) (strin // Delete a Tinkerbell Workflow. func (t *Workflow) Delete(ctx context.Context, id string) error { if _, err := t.client.DeleteWorkflow(ctx, &workflow.GetRequest{Id: id}); err != nil { - if err.Error() == sqlErrorString { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { return fmt.Errorf("workflow %w", ErrNotFound) } @@ -74,3 +87,23 @@ func (t *Workflow) Delete(ctx context.Context, id string) error { return nil } + +// HardwareToJSON converts Hardware to a string suitable for use in a +// Workflow Request for the raw Tinkerbell client. +func HardwareToJSON(h *hardware.Hardware) (string, error) { + hardwareInterfaces := h.GetNetwork().GetInterfaces() + hardwareInfo := make(map[string]string, len(hardwareInterfaces)) + + for i, hi := range hardwareInterfaces { + if mac := hi.GetDhcp().GetMac(); mac != "" { + hardwareInfo[fmt.Sprintf("device_%d", i+1)] = mac + } + } + + hardwareJSON, err := json.Marshal(hardwareInfo) + if err != nil { + return "", fmt.Errorf("failed to marshal hardware info into json: %w", err) + } + + return string(hardwareJSON), nil +} diff --git a/tink/internal/client_test/client_test.go b/tink/internal/client_test/client_test.go deleted file mode 100644 index 7caca671..00000000 --- a/tink/internal/client_test/client_test.go +++ /dev/null @@ -1,133 +0,0 @@ -/* -Copyright 2020 The Kubernetes 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 client_test - -import ( - "context" - "errors" - - "github.com/tinkerbell/tink/protos/template" - "google.golang.org/grpc" -) - -type fakeClient struct { - objs map[string]*template.WorkflowTemplate -} - -func newFakeClient(objs ...*template.WorkflowTemplate) *fakeClient { - f := &fakeClient{objs: map[string]*template.WorkflowTemplate{}} - - for i, obj := range objs { - id := obj.GetId() - - if id == "" { - obj.Id = obj.GetName() - } - - f.objs[id] = objs[i] - } - - return f -} - -func (f *fakeClient) CreateTemplate( - ctx context.Context, - in *template.WorkflowTemplate, - opts ...grpc.CallOption, -) (*template.CreateResponse, error) { - id := in.GetId() - - if id == "" { - id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - return nil, errors.New("duplicate") - } - - f.objs[id] = &template.WorkflowTemplate{ - Id: id, - Name: in.GetName(), - Data: in.GetData(), - } - - return &template.CreateResponse{Id: id}, nil -} - -func (f *fakeClient) GetTemplate( - ctx context.Context, - in *template.GetRequest, - opts ...grpc.CallOption, -) (*template.WorkflowTemplate, error) { - id := in.GetId() - if id == "" { - id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - return f.objs[id], nil - } - - return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") -} - -func (f *fakeClient) DeleteTemplate( - ctx context.Context, - in *template.GetRequest, - opts ...grpc.CallOption, -) (*template.Empty, error) { - id := in.GetId() - if id == "" { - id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - delete(f.objs, id) - - return &template.Empty{}, nil - } - - return nil, errors.New("rpc error: code = Unknown desc = sql: no rows in result set") -} - -func (f *fakeClient) ListTemplates( - ctx context.Context, - in *template.ListRequest, - opts ...grpc.CallOption, -) (template.TemplateService_ListTemplatesClient, error) { - return nil, errors.New("nobody home") -} - -func (f *fakeClient) UpdateTemplate( - ctx context.Context, - in *template.WorkflowTemplate, - opts ...grpc.CallOption, -) (*template.Empty, error) { - id := in.GetId() - - if id == "" { - in.Id = in.GetName() - } - - if _, ok := f.objs[id]; ok { - f.objs[id].Data = in.GetData() - - return &template.Empty{}, nil - } - - return nil, errors.New("nobody home") -} diff --git a/tink/internal/client_test/common_test.go b/tink/internal/client_test/common_test.go index 17f8ac9d..14bec0b0 100644 --- a/tink/internal/client_test/common_test.go +++ b/tink/internal/client_test/common_test.go @@ -17,10 +17,15 @@ limitations under the License. package client_test import ( + "crypto/rand" "crypto/x509" + "fmt" "io/ioutil" + "math/big" + "net" "net/http" "os" + "sync" "testing" . "github.com/onsi/gomega" @@ -31,6 +36,107 @@ import ( "google.golang.org/grpc/credentials" ) +const helloWorldTemplate = `version: "0.1" +name: hello_world_workflow +global_timeout: 600 +tasks: + - name: "hello world" + worker: "{{.device_1}}" + actions: + - name: "hello_world" + image: hello-world + timeout: 60` + +// These are CIDRs that we should not come across in a real +// environment, since they are reserved for use in documentation +// and examples. +var testCIDRs = [...]string{ + "192.0.2.0/24", + "198.51.100.0/24", + "203.0.113.0/24", +} + +var IPGetter = ipGetter{ + addresses: make(map[string]string), +} + +type ipGetter struct { + addresses map[string]string + lock sync.Mutex +} + +func (i *ipGetter) nextAddressFromCIDR(cidr string) (string, string, string, error) { + i.lock.Lock() + defer i.lock.Unlock() + + _, network, err := net.ParseCIDR(cidr) + if err != nil { + return "", "", "", fmt.Errorf("failed to parse cidr: %w", err) + } + + netMask := net.IP(network.Mask).String() + + // Use the first available address as the gateway address + gw := make(net.IP, len(network.IP)) + copy(gw, network.IP) + gw[len(gw)-1]++ + gateway := gw.String() + + // Attempt to get the last address used, othewise use the + // gateway address as the starting point + lastAddress, ok := i.addresses[cidr] + if !ok { + lastAddress = gateway + } + + // Get the next IP by incrementing lastAddress + nextIP := net.ParseIP(lastAddress) + nextIP[len(nextIP)-1]++ + + ip := nextIP.String() + + // Store the last address + i.addresses[cidr] = ip + + return ip, netMask, gateway, nil +} + +var MACGenerator = macGenerator{ + addresses: make(map[string]struct{}), +} + +type macGenerator struct { + addresses map[string]struct{} + lock sync.Mutex +} + +func (m *macGenerator) Get() (string, error) { + m.lock.Lock() + defer m.lock.Unlock() + + for { + mac := net.HardwareAddr(make([]byte, 6)) + + _, err := rand.Read(mac) + if err != nil { + return "", fmt.Errorf("failed to generate random mac: %w", err) + } + + // Ensure the individual bit is set + mac[0] &= ^byte(1) + + // Ensure the local bit is set + mac[0] |= byte(2) + + key := mac.String() + if _, found := m.addresses[key]; !found { + m.addresses[key] = struct{}{} + + return key, nil + } + } +} + func generateTemplate(name, data string) *template.WorkflowTemplate { return &template.WorkflowTemplate{ Name: name, @@ -38,6 +144,59 @@ func generateTemplate(name, data string) *template.WorkflowTemplate { } } +func generateHardware(numInterfaces int) (*hardware.Hardware, error) { + hw := &hardware.Hardware{ + Network: &hardware.Hardware_Network{}, + } + + for i := 0; i < numInterfaces; i++ { + cidr := testCIDRs[i%len(testCIDRs)] + + ni, err := generateHardwareInterface(cidr) + if err != nil { + return nil, err + } + + hw.Network.Interfaces = append(hw.Network.Interfaces, ni) + } + + return hw, nil +} + +func generateHardwareInterface(cidr string) (*hardware.Hardware_Network_Interface, error) { + if cidr == "" { + i, err := rand.Int(rand.Reader, big.NewInt(int64(len(testCIDRs)))) + if err != nil { + return nil, fmt.Errorf("failed to get random index for cidr: %w", err) + } + + cidr = testCIDRs[i.Int64()] + } + + ip, netmask, gateway, err := IPGetter.nextAddressFromCIDR(cidr) + if err != nil { + return nil, err + } + + mac, err := MACGenerator.Get() + if err != nil { + return nil, err + } + + ni := &hardware.Hardware_Network_Interface{ + Dhcp: &hardware.Hardware_DHCP{ + Mac: mac, + Ip: &hardware.Hardware_DHCP_IP{ + Address: ip, + Netmask: netmask, + Gateway: gateway, + }, + }, + } + + return ni, nil +} + func realConn(t *testing.T) *grpc.ClientConn { g := NewWithT(t) diff --git a/tink/internal/client_test/hardware_test.go b/tink/internal/client_test/hardware_test.go new file mode 100644 index 00000000..6c6d4302 --- /dev/null +++ b/tink/internal/client_test/hardware_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2020 The Kubernetes 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 client_test + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" +) + +func TestHardwareLifecycle(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + hardwareClient := client.NewHardwareClient(realHardwareClient(t)) + + // Create a Hardware resource in Tinkerbell + testHardware, err := generateHardware(2) + g.Expect(err).NotTo(HaveOccurred()) + + hardwareMACs := make([]string, 0, len(testHardware.Network.Interfaces)) + hardwareIPs := make([]string, 0, len(testHardware.Network.Interfaces)) + + for _, i := range testHardware.Network.Interfaces { + hardwareMACs = append(hardwareMACs, i.Dhcp.Mac) + hardwareIPs = append(hardwareIPs, i.Dhcp.Ip.Address) + } + + g.Expect(hardwareClient.Create(ctx, testHardware)).To(Succeed()) + + // Ensure that the template now has an ID set + g.Expect(testHardware.Id).NotTo(BeEmpty()) + expectedID := testHardware.Id + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we can delete the template by ID + g.Expect(hardwareClient.Delete(ctx, expectedID)) + + // Ensure that we now get a NotFound error trying to get the template + _, err := hardwareClient.Get(ctx, expectedID, "", "") + g.Expect(err).To(MatchError(client.ErrNotFound)) + }() + + // Ensure that we can get the hardware by ID and values match what we expect + res, err := hardwareClient.Get(ctx, expectedID, "", "") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).NotTo(BeNil()) + g.Expect(res.GetId()).To(BeEquivalentTo(expectedID)) + g.Expect(res.GetMetadata()).To(BeEquivalentTo(testHardware.GetMetadata())) + + resInterfaces := res.GetNetwork().GetInterfaces() + resMACs := make([]string, 0, len(resInterfaces)) + resIPs := make([]string, 0, len(resInterfaces)) + + for _, i := range res.GetNetwork().GetInterfaces() { + resMACs = append(resMACs, i.GetDhcp().GetMac()) + resIPs = append(resIPs, i.GetDhcp().GetIp().GetAddress()) + } + + g.Expect(resMACs).To(ConsistOf(hardwareMACs)) + g.Expect(resIPs).To(ConsistOf(hardwareIPs)) + + // Ensure that we can get the hardware by mac + for _, mac := range hardwareMACs { + res, err := hardwareClient.Get(ctx, "", "", mac) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).NotTo(BeNil()) + g.Expect(res.GetId()).To(BeEquivalentTo(expectedID)) + } + + // Ensure that we can get the hardware by ip + for _, ip := range hardwareIPs { + res, err := hardwareClient.Get(ctx, "", ip, "") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).NotTo(BeNil()) + g.Expect(res.GetId()).To(BeEquivalentTo(expectedID)) + } + + // Ensure that we can update the hardware in Tinkerbell + additionalInterface, err := generateHardwareInterface("") + g.Expect(err).NotTo(HaveOccurred()) + + testHardware.Network.Interfaces = append(testHardware.Network.Interfaces, additionalInterface) + hardwareMACs = append(hardwareMACs, additionalInterface.Dhcp.Mac) + hardwareIPs = append(hardwareIPs, additionalInterface.Dhcp.Ip.Address) + + g.Expect(hardwareClient.Update(ctx, testHardware)).To(Succeed()) + + // Ensure that the hardware was updated in Tinkerbell + res, err = hardwareClient.Get(ctx, expectedID, "", "") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).NotTo(BeNil()) + g.Expect(res.GetId()).To(BeEquivalentTo(expectedID)) + g.Expect(res.GetMetadata()).To(BeEquivalentTo(testHardware.GetMetadata())) + + resInterfaces = res.GetNetwork().GetInterfaces() + resMACs = make([]string, 0, len(resInterfaces)) + resIPs = make([]string, 0, len(resInterfaces)) + + for _, i := range res.GetNetwork().GetInterfaces() { + resMACs = append(resMACs, i.GetDhcp().GetMac()) + resIPs = append(resIPs, i.GetDhcp().GetIp().GetAddress()) + } + + g.Expect(resMACs).To(ConsistOf(hardwareMACs)) + g.Expect(resIPs).To(ConsistOf(hardwareIPs)) +} diff --git a/tink/internal/client_test/template_test.go b/tink/internal/client_test/template_test.go index 2454f0fa..793faa42 100644 --- a/tink/internal/client_test/template_test.go +++ b/tink/internal/client_test/template_test.go @@ -26,16 +26,6 @@ import ( ) const ( - helloWorldTemplate = `version: "0.1" -name: hello_world_workflow -global_timeout: 600 -tasks: - - name: "hello world" - worker: "{{.device_1}}" - actions: - - name: "hello_world" - image: hello-world - timeout: 60` sampleTemplate = ` version: '0.1' name: sample_workflow diff --git a/tink/internal/client_test/workflow_test.go b/tink/internal/client_test/workflow_test.go index 60136a0b..8210b3ec 100644 --- a/tink/internal/client_test/workflow_test.go +++ b/tink/internal/client_test/workflow_test.go @@ -30,7 +30,8 @@ func TestWorkflowLifecycle(t *testing.T) { ctx := context.Background() templateClient := client.NewTemplateClient(realTemplateClient(t)) - _ = realWorkflowClient(t) + hardwareClient := client.NewHardwareClient(realHardwareClient(t)) + workflowClient := client.NewWorkflowClient(realWorkflowClient(t), hardwareClient) // Create a template for the workflow to use testTemplate := generateTemplate(rand.String(12), helloWorldTemplate) @@ -38,17 +39,37 @@ func TestWorkflowLifecycle(t *testing.T) { // Attempt to cleanup even if later assertions fail defer func() { - // Ensure that we can delete the template by ID g.Expect(templateClient.Delete(ctx, testTemplate.Id)) }() - // TODO: Create hardware for the workflow to use + // Create hardware for the workflow to use + testHardware, err := generateHardware(3) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(hardwareClient.Create(ctx, testHardware)).To(Succeed()) - // TODO: Create the workflow + // Attempt to cleanup even if later assertions fail + defer func() { + g.Expect(hardwareClient.Delete(ctx, testHardware.Id)) + }() + + // Create the workflow + workflowID, err := workflowClient.Create(ctx, testTemplate.Id, testHardware.Id) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(workflowID).NotTo(BeEmpty()) + + // Attempt to cleanup even if later assertions fail + defer func() { + g.Expect(workflowClient.Delete(ctx, workflowID)) + }() - // TODO: Ensure we can get the workflow we just created by ID - // and that it has the values we expect + // Get the workflow and verify the values are what we expect + res, err := workflowClient.Get(ctx, workflowID) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res).NotTo(BeNil()) + g.Expect(res.GetId()).To(BeEquivalentTo(workflowID)) + g.Expect(res.GetTemplate()).To(BeEquivalentTo(testTemplate.Id)) - // TODO: Delete the workflow - t.Error("not ready yet") + expectedHardwareJSON, err := client.HardwareToJSON(testHardware) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res.GetHardware()).To(MatchJSON(expectedHardwareJSON)) } diff --git a/tink/internal/controllers/hardware/controller.go b/tink/internal/controllers/hardware/controller.go index fb8ee58e..cdfa8467 100644 --- a/tink/internal/controllers/hardware/controller.go +++ b/tink/internal/controllers/hardware/controller.go @@ -23,22 +23,25 @@ import ( "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" "github.com/tinkerbell/tink/protos/hardware" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +type hardwareClient interface { + // Create(ctx context.Context, h *hardware.Hardware) error + // Update(ctx context.Context, h *hardware.Hardware) error + Get(ctx context.Context, id, ip, mac string) (*hardware.Hardware, error) + // Delete(ctx context.Context, id string) error +} + // Reconciler implements Reconciler interface by managing Tinkerbell hardware. type Reconciler struct { client.Client - HardwareClient hardware.HardwareServiceClient + HardwareClient hardwareClient Log logr.Logger - Recorder record.EventRecorder Scheme *runtime.Scheme } @@ -68,67 +71,25 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, fmt.Errorf("failed to get hardware: %w", err) } - // Ensure that we add the finalizer to the resource - err := common.EnsureFinalizer(ctx, r.Client, logger, hardware, tinkv1alpha1.HardwareFinalizer) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to ensure finalizer on hardware: %w", err) - } - - // Handle deleted hardware. + // Deletion is a noop. if !hardware.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, hardware) + return ctrl.Result{}, nil } - return r.reconcileNormal(hardware) + return r.reconcileNormal(ctx, hardware) } -func (r *Reconciler) reconcileNormal(hardware *tinkv1alpha1.Hardware) (ctrl.Result, error) { - logger := r.Log.WithValues("hardware", hardware.Name) - err := common.ErrNotImplemented - - logger.Error(err, "Not yet implemented") - - return ctrl.Result{}, err -} - -func (r *Reconciler) reconcileDelete(ctx context.Context, h *tinkv1alpha1.Hardware) (ctrl.Result, error) { - // Create a patch for use later - patch := client.MergeFrom(h.DeepCopy()) - +func (r *Reconciler) reconcileNormal(ctx context.Context, h *tinkv1alpha1.Hardware) (ctrl.Result, error) { logger := r.Log.WithValues("hardware", h.Name) - if h.Annotations == nil { - h.Annotations = map[string]string{} - } - - id, ok := h.Annotations[tinkv1alpha1.HardwareIDAnnotation] - if !ok { - // TODO: figure out how to lookup a hardware without an ID - logger.Error(common.ErrNotImplemented, "Unable to delete a hardware without having recorded the ID") - - return ctrl.Result{}, common.ErrNotImplemented - } - - hardwareRequest := &hardware.DeleteRequest{ - Id: id, - } - - _, err := r.HardwareClient.Delete(ctx, hardwareRequest) - // TODO: Tinkerbell should return some type of status that is easier to handle - // than parsing for this specific error message. - if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { - logger.Error(err, "Failed to delete hardware from Tinkerbell") + tinkHardware, err := r.HardwareClient.Get(ctx, h.Spec.ID, "", "") + if err != nil { + logger.Error(err, "Failed to get hardware from Tinkerbell") - return ctrl.Result{}, fmt.Errorf("failed to delete hardware from Tinkerbell: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to get hardware from Tinkerbell: %w", err) } - controllerutil.RemoveFinalizer(h, tinkv1alpha1.HardwareFinalizer) - - if err := r.Client.Patch(ctx, h, patch); err != nil { - logger.Error(err, "Failed to patch hardware") - - return ctrl.Result{}, fmt.Errorf("failed to patch hardware: %w", err) - } + logger.Info("Found hardware in tinkerbell", "tinkHardware", tinkHardware) return ctrl.Result{}, nil } diff --git a/tink/internal/controllers/template/controller.go b/tink/internal/controllers/template/controller.go index 516fae4a..1c080918 100644 --- a/tink/internal/controllers/template/controller.go +++ b/tink/internal/controllers/template/controller.go @@ -35,17 +35,17 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -type TemplateClient interface { +type templateClient interface { Get(ctx context.Context, id, name string) (*template.WorkflowTemplate, error) Create(ctx context.Context, template *template.WorkflowTemplate) error Update(ctx context.Context, template *template.WorkflowTemplate) error - Delete(ctx context.Context, id, name string) error + Delete(ctx context.Context, id string) error } // Reconciler implements the Reconciler interface for managing Tinkerbell templates. type Reconciler struct { client.Client - TemplateClient TemplateClient + TemplateClient templateClient Log logr.Logger Scheme *runtime.Scheme } @@ -177,14 +177,14 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, t *tinkv1alpha1.Templa annotations = map[string]string{} } - err := r.TemplateClient.Delete(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation], t.Name) + // If we've recorded an ID for the Template, then we should delete it + if id, ok := annotations[tinkv1alpha1.TemplateIDAnnotation]; ok { + err := r.TemplateClient.Delete(ctx, id) + if err != nil && !errors.Is(err, tinkclient.ErrNotFound) { + logger.Error(err, "Failed to delete template from Tinkerbell") - // TODO: Tinkerbell should return some type of status that is easier to handle - // than parsing for this specific error message. - if err != nil && !errors.Is(err, tinkclient.ErrNotFound) { - logger.Error(err, "Failed to delete template from Tinkerbell") - - return ctrl.Result{}, fmt.Errorf("failed to delete template from Tinkerbell: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to delete template from Tinkerbell: %w", err) + } } controllerutil.RemoveFinalizer(t, tinkv1alpha1.TemplateFinalizer) diff --git a/tink/internal/controllers/template/controller_internal_test.go b/tink/internal/controllers/template/controller_internal_test.go index 004a88d9..126ab9ce 100644 --- a/tink/internal/controllers/template/controller_internal_test.go +++ b/tink/internal/controllers/template/controller_internal_test.go @@ -59,24 +59,6 @@ func TestTemplateReconciler_reconcileDelete(t *testing.T) { want ctrl.Result wantErr bool }{ - { - name: "successful delete by name", - in: &tinkv1alpha1.Template{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - DeletionTimestamp: &now, - Finalizers: []string{tinkv1alpha1.TemplateFinalizer}, - }, - }, - tinkObjs: []*template.WorkflowTemplate{ - { - Id: "test", - Name: "test", - }, - }, - want: ctrl.Result{}, - wantErr: false, - }, { name: "successful delete by id", in: &tinkv1alpha1.Template{ diff --git a/tink/internal/controllers/workflow/controller.go b/tink/internal/controllers/workflow/controller.go index cb84914f..ae1ff441 100644 --- a/tink/internal/controllers/workflow/controller.go +++ b/tink/internal/controllers/workflow/controller.go @@ -19,26 +19,32 @@ package controllers import ( "context" + "errors" "fmt" "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" "github.com/tinkerbell/tink/protos/workflow" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +type workflowClient interface { + Get(ctx context.Context, id string) (*workflow.Workflow, error) + Create(ctx context.Context, templateID, hardwareID string) (string, error) + Delete(ctx context.Context, id string) error +} + // WorkflowReconciler implements Reconciler interface by managing Tinkerbell workflows. type WorkflowReconciler struct { client.Client - WorkflowClient workflow.WorkflowServiceClient + WorkflowClient workflowClient Log logr.Logger - Recorder record.EventRecorder Scheme *runtime.Scheme } @@ -69,8 +75,7 @@ func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } // Ensure that we add the finalizer to the resource - err := common.EnsureFinalizer(ctx, r.Client, logger, workflow, tinkv1alpha1.WorkflowFinalizer) - if err != nil { + if err := common.EnsureFinalizer(ctx, r.Client, logger, workflow, tinkv1alpha1.WorkflowFinalizer); err != nil { return ctrl.Result{}, fmt.Errorf("failed to ensure finalizer on workflow: %w", err) } @@ -79,47 +84,101 @@ func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return r.reconcileDelete(ctx, workflow) } - return r.reconcileNormal(workflow) + return r.reconcileNormal(ctx, workflow) } -func (r *WorkflowReconciler) reconcileNormal(workflow *tinkv1alpha1.Workflow) (ctrl.Result, error) { - logger := r.Log.WithValues("workflow", workflow.Name) - err := common.ErrNotImplemented +func (r *WorkflowReconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { + logger := r.Log.WithValues("workflow", w.Name) - logger.Error(err, "Not yet implemented") + var workflowID string - return ctrl.Result{}, err -} + tinkWorkflow, err := r.getWorkflow(ctx, w) -func (r *WorkflowReconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { - // Create a patch for use later - patch := client.MergeFrom(w.DeepCopy()) + switch { + case errors.Is(err, tinkclient.ErrNotFound): + id, err := r.createWorkflow(ctx, w) + if err != nil { + return ctrl.Result{}, err + } + + workflowID = id + case err != nil: + return ctrl.Result{}, err + default: + workflowID = tinkWorkflow.Id + } + + if err := common.EnsureAnnotation(ctx, r.Client, logger, w, tinkv1alpha1.WorkflowIDAnnotation, + workflowID); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to ensure id annotation on workflow: %w", err) + } + + return ctrl.Result{}, nil +} + +func (r *WorkflowReconciler) createWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (string, error) { logger := r.Log.WithValues("workflow", w.Name) - if w.Annotations == nil { - w.Annotations = map[string]string{} + hw := &tinkv1alpha1.Hardware{} + hwKey := client.ObjectKey{Name: w.Spec.HardwareRef} + + if err := r.Client.Get(ctx, hwKey, hw); err != nil { + logger.Error(err, "Failed to get hardware") + + return "", fmt.Errorf("failed to get hardware: %w", err) } - id, ok := w.Annotations[tinkv1alpha1.WorkflowIDAnnotation] - if !ok { - // TODO: figure out how to lookup a workflow without an ID - logger.Error(common.ErrNotImplemented, "Unable to delete a workflow without having recorded the ID") + t := &tinkv1alpha1.Template{} + tKey := client.ObjectKey{Name: w.Spec.TemplateRef} - return ctrl.Result{}, common.ErrNotImplemented + if err := r.Client.Get(ctx, tKey, t); err != nil { + logger.Error(err, "Failed to get template") + + return "", fmt.Errorf("failed to get template: %w", err) + } + + id, err := r.WorkflowClient.Create( + ctx, t.GetObjectMeta().GetAnnotations()[tinkv1alpha1.TemplateIDAnnotation], + hw.Spec.ID, + ) + if err != nil { + logger.Error(err, "Failed to create workflow") + + return "", fmt.Errorf("failed to create workflow: %w", err) + } + + return id, nil +} + +func (r *WorkflowReconciler) getWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (*workflow.Workflow, error) { + annotations := w.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} } - workflowRequest := &workflow.GetRequest{ - Id: id, + return r.WorkflowClient.Get(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation]) +} + +func (r *WorkflowReconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { + // Create a patch for use later + patch := client.MergeFrom(w.DeepCopy()) + + logger := r.Log.WithValues("workflow", w.Name) + + annotations := w.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} } - _, err := r.WorkflowClient.DeleteWorkflow(ctx, workflowRequest) - // TODO: Tinkerbell should return some type of status that is easier to handle - // than parsing for this specific error message. - if err != nil && err.Error() != "rpc error: code = Unknown desc = sql: no rows in result set" { - logger.Error(err, "Failed to delete workflow from Tinkerbell") + // If we've recorded an ID for the Workflow, then we should delete it + if id, ok := annotations[tinkv1alpha1.WorkflowIDAnnotation]; ok { + err := r.WorkflowClient.Delete(ctx, id) + if err != nil && !errors.Is(err, tinkclient.ErrNotFound) { + logger.Error(err, "Failed to delete workflow from Tinkerbell") - return ctrl.Result{}, fmt.Errorf("failed to delete workflow from Tinkerbell: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to delete workflow from Tinkerbell: %w", err) + } } controllerutil.RemoveFinalizer(w, tinkv1alpha1.WorkflowFinalizer) From dab620e91388e1f4f576e8baf13730ba4c0abd9f Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Mon, 21 Dec 2020 17:30:39 -0500 Subject: [PATCH 09/15] Start wiring up controllers and event handlers --- Makefile | 2 +- go.mod | 1 + go.sum | 1 + main.go | 221 +++++++++++++++++- tink/api/v1alpha1/groupversion_info.go | 5 +- tink/api/v1alpha1/hardware_types.go | 5 + tink/api/v1alpha1/template_types.go | 10 + tink/api/v1alpha1/workflow_types.go | 10 + tink/{internal => }/client/common.go | 6 +- tink/{internal => }/client/fake/hardware.go | 2 +- tink/{internal => }/client/fake/template.go | 2 +- tink/{internal => }/client/fake/workflow.go | 2 +- tink/{internal => }/client/hardware.go | 4 +- tink/{internal => }/client/template.go | 4 +- tink/{internal => }/client/workflow.go | 6 +- .../{internal => }/client_test/common_test.go | 0 .../client_test/hardware_test.go | 2 +- .../client_test/template_test.go | 2 +- .../client_test/workflow_test.go | 2 +- .../controllers/common/common.go | 0 .../controllers/common_test/common_test.go | 2 +- .../controllers/hardware/controller.go | 9 +- .../controllers/template/controller.go | 13 +- .../template/controller_internal_test.go | 2 +- .../controllers/workflow/controller.go | 27 ++- 25 files changed, 300 insertions(+), 40 deletions(-) rename tink/{internal => }/client/common.go (83%) rename tink/{internal => }/client/fake/hardware.go (97%) rename tink/{internal => }/client/fake/template.go (97%) rename tink/{internal => }/client/fake/workflow.go (96%) rename tink/{internal => }/client/hardware.go (96%) rename tink/{internal => }/client/template.go (95%) rename tink/{internal => }/client/workflow.go (96%) rename tink/{internal => }/client_test/common_test.go (100%) rename tink/{internal => }/client_test/hardware_test.go (98%) rename tink/{internal => }/client_test/template_test.go (97%) rename tink/{internal => }/client_test/workflow_test.go (96%) rename tink/{internal => }/controllers/common/common.go (100%) rename tink/{internal => }/controllers/common_test/common_test.go (97%) rename tink/{internal => }/controllers/hardware/controller.go (90%) rename tink/{internal => }/controllers/template/controller.go (93%) rename tink/{internal => }/controllers/template/controller_internal_test.go (99%) rename tink/{internal => }/controllers/workflow/controller.go (83%) diff --git a/Makefile b/Makefile index 06f27f09..a3476aed 100644 --- a/Makefile +++ b/Makefile @@ -294,7 +294,7 @@ generate-manifests: $(CONTROLLER_GEN) # Generate manifests e.g. CRD, RBAC etc. paths=./api/... \ paths=./controllers/... \ paths=./tink/api/... \ - paths=./tink/internal/controllers/... \ + paths=./tink/controllers/... \ crd:crdVersions=v1 \ rbac:roleName=manager-role \ output:crd:dir=./config/crd/bases \ diff --git a/go.mod b/go.mod index b6dfd5db..cff00b32 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( k8s.io/api v0.17.16 k8s.io/apimachinery v0.17.16 k8s.io/client-go v0.17.16 + k8s.io/klog v1.0.0 k8s.io/utils v0.0.0-20201110183641-67b214c5f920 sigs.k8s.io/cluster-api v0.3.12 sigs.k8s.io/controller-runtime v0.5.14 diff --git a/go.sum b/go.sum index 23949702..92ec5212 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/containerd v1.3.2 h1:ForxmXkA6tPIvffbrDAcPUIB32QgXkt2XFj+F0UxetA= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/coredns/corefile-migration v1.0.11/go.mod h1:RMy/mXdeDlYwzt0vdMEJvT2hGJ2I86/eO0UdXmH9XNI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/main.go b/main.go index 764c705b..d9775bac 100644 --- a/main.go +++ b/main.go @@ -16,21 +16,37 @@ limitations under the License. package main import ( + "context" "errors" "flag" + "fmt" + "io" "os" "time" + "github.com/go-logr/logr" "github.com/tinkerbell/cluster-api-provider-tinkerbell/controllers" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + tinkhardware "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/hardware" + tinktemplate "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/template" + tinkworkflow "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/workflow" + tinkclient "github.com/tinkerbell/tink/client" + tinkinformers "github.com/tinkerbell/tink/client/informers" + tinkevents "github.com/tinkerbell/tink/protos/events" + "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/client-go/tools/record" + "k8s.io/klog" + "k8s.io/klog/klogr" ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/log/zap" infrastructurev1alpha3 "github.com/tinkerbell/cluster-api-provider-tinkerbell/api/v1alpha3" + tinkv1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" // +kubebuilder:scaffold:imports ) @@ -43,9 +59,12 @@ var ( //nolint:wsl,gochecknoinits func init() { + klog.InitFlags(nil) + _ = clientgoscheme.AddToScheme(scheme) _ = infrastructurev1alpha3.AddToScheme(scheme) _ = clusterv1.AddToScheme(scheme) + _ = tinkv1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } @@ -104,7 +123,7 @@ func main() { flag.Parse() - ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + ctrl.SetLogger(klogr.New()) if watchNamespace != "" { setupLog.Info("Watching cluster-api objects only in namespace for reconciliation", "namespace", watchNamespace) @@ -133,13 +152,86 @@ func main() { os.Exit(1) } - // TODO: Get a Tinkerbell client. + if err := tinkclient.Setup(); err != nil { + setupLog.Error(err, "unable to create tinkerbell client") + os.Exit(1) + } + + hwClient := client.NewHardwareClient(tinkclient.HardwareClient) + templateClient := client.NewTemplateClient(tinkclient.TemplateClient) + workflowClient := client.NewWorkflowClient(tinkclient.WorkflowClient, hwClient) + + stopCh := ctrl.SetupSignalHandler() if webhookPort == 0 { + hwChan := make(chan event.GenericEvent) + templateChan := make(chan event.GenericEvent) + workflowChan := make(chan event.GenericEvent) + + if err := mgr.Add(&tinkEventWatcher{ + k8sClient: mgr.GetClient(), + eventCh: hwChan, + logger: ctrl.Log.WithName("tinkwatcher").WithName("Hardware"), + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE, + }); err != nil { + setupLog.Error(err, "unable to create tink watcher", "tinkwatcher", "Hardware") + os.Exit(1) + } + + if err := mgr.Add(&tinkEventWatcher{ + k8sClient: mgr.GetClient(), + eventCh: templateChan, + logger: ctrl.Log.WithName("tinkwatcher").WithName("Template"), + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE, + }); err != nil { + setupLog.Error(err, "unable to create tink watcher", "tinkwatcher", "Template") + os.Exit(1) + } + + if err := mgr.Add(&tinkEventWatcher{ + k8sClient: mgr.GetClient(), + eventCh: workflowChan, + logger: ctrl.Log.WithName("tinkwatcher").WithName("Workflow"), + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW, + }); err != nil { + setupLog.Error(err, "unable to create tink watcher", "tinkwatcher", "Workflow") + os.Exit(1) + } + + if err = (&tinkhardware.Reconciler{ + Client: mgr.GetClient(), + HardwareClient: hwClient, + Log: ctrl.Log.WithName("controllers").WithName("Hardware"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, hwChan); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Hardware") + os.Exit(1) + } + + if err = (&tinktemplate.Reconciler{ + Client: mgr.GetClient(), + TemplateClient: templateClient, + Log: ctrl.Log.WithName("controllers").WithName("Template"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, templateChan); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Template") + os.Exit(1) + } + + if err = (&tinkworkflow.Reconciler{ + Client: mgr.GetClient(), + WorkflowClient: workflowClient, + Log: ctrl.Log.WithName("controllers").WithName("Workflow"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, workflowChan); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Workflow") + os.Exit(1) + } + if err = (&controllers.TinkerbellClusterReconciler{ Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("TinerellCluster"), - Recorder: mgr.GetEventRecorderFor("tinerellcluster-controller"), + Log: ctrl.Log.WithName("controllers").WithName("TinkerbellCluster"), + Recorder: mgr.GetEventRecorderFor("tinkerbellcluster-controller"), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "TinkerbellCluster") @@ -174,8 +266,125 @@ func main() { setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(stopCh); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } + +type tinkEventWatcher struct { + k8sClient ctrlclient.Client + eventCh chan<- event.GenericEvent + logger logr.Logger + resourceType tinkevents.ResourceType +} + +func (w *tinkEventWatcher) getHardwareForID(ctx context.Context, id string) (*tinkv1.Hardware, error) { + hwList := &tinkv1.HardwareList{} + if err := w.k8sClient.List(ctx, hwList); err != nil { + return nil, err + } + + for i, h := range hwList.Items { + if h.TinkID() == id { + return &hwList.Items[i], nil + } + } + + return nil, nil +} + +func (w *tinkEventWatcher) getTemplateForID(ctx context.Context, id string) (*tinkv1.Template, error) { + templateList := &tinkv1.TemplateList{} + if err := w.k8sClient.List(ctx, templateList); err != nil { + return nil, err + } + + for i, t := range templateList.Items { + if t.TinkID() == id { + return &templateList.Items[i], nil + } + } + + return nil, nil +} + +func (w *tinkEventWatcher) getWorkflowForID(ctx context.Context, id string) (*tinkv1.Workflow, error) { + workflowList := &tinkv1.WorkflowList{} + if err := w.k8sClient.List(ctx, workflowList); err != nil { + return nil, err + } + + for i, w := range workflowList.Items { + if w.TinkID() == id { + return &workflowList.Items[i], nil + } + } + + return nil, nil +} + +func (w *tinkEventWatcher) generateEventForTinkID(ctx context.Context, id string) error { + switch w.resourceType { + case tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE: + hw, _ := w.getHardwareForID(ctx, id) + w.eventCh <- event.GenericEvent{ + Meta: hw, + Object: hw, + } + case tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE: + template, _ := w.getTemplateForID(ctx, id) + w.eventCh <- event.GenericEvent{ + Meta: template, + Object: template, + } + case tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW: + workflow, _ := w.getWorkflowForID(ctx, id) + w.eventCh <- event.GenericEvent{ + Meta: workflow, + Object: workflow, + } + default: + return fmt.Errorf("unknown resource type: %s", w.resourceType.String()) + } + + return nil +} + +func (w *tinkEventWatcher) Start(stopCh <-chan struct{}) error { + now := time.Now() + + req := &tinkevents.WatchRequest{ + EventTypes: []tinkevents.EventType{ + tinkevents.EventType_EVENT_TYPE_CREATED, + tinkevents.EventType_EVENT_TYPE_UPDATED, + tinkevents.EventType_EVENT_TYPE_DELETED, + }, + ResourceTypes: []tinkevents.ResourceType{w.resourceType}, + WatchEventsFrom: timestamppb.New(now), + } + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-stopCh + cancel() + }() + + tinkInformer := tinkinformers.New() + + w.logger.Info("Starting Tinkerbell Informer", "resourceType", w.resourceType.String()) + + err := tinkInformer.Start(ctx, req, func(e *tinkevents.Event) error { + if err := w.generateEventForTinkID(ctx, e.GetResourceId()); err != nil { + return err + } + + return nil + }) + if err != nil && !errors.Is(err, io.EOF) { + return err + } + + return nil +} diff --git a/tink/api/v1alpha1/groupversion_info.go b/tink/api/v1alpha1/groupversion_info.go index 6ad9e194..5b7a9608 100644 --- a/tink/api/v1alpha1/groupversion_info.go +++ b/tink/api/v1alpha1/groupversion_info.go @@ -13,6 +13,7 @@ limitations under the License. // +kubebuilder:object:generate=true // +groupName=tinkerbell.org + package v1alpha1 import ( @@ -21,10 +22,10 @@ import ( ) var ( - // GroupVersion is group version used to register these objects + // GroupVersion is group version used to register these objects. GroupVersion = schema.GroupVersion{Group: "tinkerbell.org", Version: "v1alpha1"} - // SchemeBuilder is used to add go types to the GroupVersionKind scheme + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. diff --git a/tink/api/v1alpha1/hardware_types.go b/tink/api/v1alpha1/hardware_types.go index a0517317..6b2f30fc 100644 --- a/tink/api/v1alpha1/hardware_types.go +++ b/tink/api/v1alpha1/hardware_types.go @@ -45,6 +45,11 @@ type Hardware struct { Status HardwareStatus `json:"status,omitempty"` } +// TinkID returns the Tinkerbell ID associated with this Hardware. +func (h *Hardware) TinkID() string { + return h.Spec.ID +} + // +kubebuilder:object:root=true // HardwareList contains a list of Hardware. diff --git a/tink/api/v1alpha1/template_types.go b/tink/api/v1alpha1/template_types.go index c3e185ae..3c92631a 100644 --- a/tink/api/v1alpha1/template_types.go +++ b/tink/api/v1alpha1/template_types.go @@ -52,6 +52,16 @@ type Template struct { Status TemplateStatus `json:"status,omitempty"` } +// TinkID returns the Tinkerbell ID associated with this Template. +func (t *Template) TinkID() string { + annotations := t.GetAnnotations() + if len(annotations) == 0 { + return "" + } + + return annotations[TemplateIDAnnotation] +} + // +kubebuilder:object:root=true // TemplateList contains a list of Templates. diff --git a/tink/api/v1alpha1/workflow_types.go b/tink/api/v1alpha1/workflow_types.go index 974197f9..66249aea 100644 --- a/tink/api/v1alpha1/workflow_types.go +++ b/tink/api/v1alpha1/workflow_types.go @@ -63,6 +63,16 @@ type Workflow struct { Status WorkflowStatus `json:"status,omitempty"` } +// TinkID returns the Tinkerbell ID associated with this Workflow. +func (w *Workflow) TinkID() string { + annotations := w.GetAnnotations() + if len(annotations) == 0 { + return "" + } + + return annotations[WorkflowIDAnnotation] +} + // +kubebuilder:object:root=true // WorkflowList contains a list of Workflows. diff --git a/tink/internal/client/common.go b/tink/client/common.go similarity index 83% rename from tink/internal/client/common.go rename to tink/client/common.go index 81437764..fa350602 100644 --- a/tink/internal/client/common.go +++ b/tink/client/common.go @@ -26,5 +26,7 @@ var ErrNotFound = errors.New("resource not found") // TODO: Tinkerbell should return some type of status that is easier to handle // than parsing for these specific error message. -const sqlErrorString = "rpc error: code = Unknown desc = sql: no rows in result set" -const sqlErrorStringAlt = "rpc error: code = Unknown desc = SELECT: sql: no rows in result set" +const ( + sqlErrorString = "rpc error: code = Unknown desc = sql: no rows in result set" + sqlErrorStringAlt = "rpc error: code = Unknown desc = SELECT: sql: no rows in result set" +) diff --git a/tink/internal/client/fake/hardware.go b/tink/client/fake/hardware.go similarity index 97% rename from tink/internal/client/fake/hardware.go rename to tink/client/fake/hardware.go index b2581e6d..9ec55dc6 100644 --- a/tink/internal/client/fake/hardware.go +++ b/tink/client/fake/hardware.go @@ -22,7 +22,7 @@ import ( "errors" "github.com/google/uuid" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "github.com/tinkerbell/tink/protos/hardware" "google.golang.org/protobuf/proto" ) diff --git a/tink/internal/client/fake/template.go b/tink/client/fake/template.go similarity index 97% rename from tink/internal/client/fake/template.go rename to tink/client/fake/template.go index c881d4ab..ec7da339 100644 --- a/tink/internal/client/fake/template.go +++ b/tink/client/fake/template.go @@ -22,7 +22,7 @@ import ( "errors" "github.com/google/uuid" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "github.com/tinkerbell/tink/protos/template" "google.golang.org/protobuf/proto" ) diff --git a/tink/internal/client/fake/workflow.go b/tink/client/fake/workflow.go similarity index 96% rename from tink/internal/client/fake/workflow.go rename to tink/client/fake/workflow.go index dcde19a0..c7e85433 100644 --- a/tink/internal/client/fake/workflow.go +++ b/tink/client/fake/workflow.go @@ -21,7 +21,7 @@ import ( "context" "github.com/google/uuid" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "github.com/tinkerbell/tink/protos/workflow" "google.golang.org/protobuf/proto" ) diff --git a/tink/internal/client/hardware.go b/tink/client/hardware.go similarity index 96% rename from tink/internal/client/hardware.go rename to tink/client/hardware.go index 976a3724..c7057faf 100644 --- a/tink/internal/client/hardware.go +++ b/tink/client/hardware.go @@ -32,8 +32,8 @@ type Hardware struct { } // NewHardwareClient returns a Hardware client. -func NewHardwareClient(client hardware.HardwareServiceClient) Hardware { - return Hardware{client: client} +func NewHardwareClient(client hardware.HardwareServiceClient) *Hardware { + return &Hardware{client: client} } // Create Tinkerbell Hardware. diff --git a/tink/internal/client/template.go b/tink/client/template.go similarity index 95% rename from tink/internal/client/template.go rename to tink/client/template.go index abc84098..81d406a3 100644 --- a/tink/internal/client/template.go +++ b/tink/client/template.go @@ -29,8 +29,8 @@ type Template struct { } // NewTemplateClient returns a Template client. -func NewTemplateClient(client template.TemplateServiceClient) Template { - return Template{client: client} +func NewTemplateClient(client template.TemplateServiceClient) *Template { + return &Template{client: client} } // Get returns a Tinkerbell Template. diff --git a/tink/internal/client/workflow.go b/tink/client/workflow.go similarity index 96% rename from tink/internal/client/workflow.go rename to tink/client/workflow.go index 85db8b23..347d18a0 100644 --- a/tink/internal/client/workflow.go +++ b/tink/client/workflow.go @@ -28,12 +28,12 @@ import ( // Workflow client for Tinkerbell. type Workflow struct { client workflow.WorkflowServiceClient - hardwareClient Hardware + hardwareClient *Hardware } // NewWorkflowClient returns a Workflow client. -func NewWorkflowClient(client workflow.WorkflowServiceClient, hClient Hardware) Workflow { - return Workflow{client: client, hardwareClient: hClient} +func NewWorkflowClient(client workflow.WorkflowServiceClient, hClient *Hardware) *Workflow { + return &Workflow{client: client, hardwareClient: hClient} } // Get returns a Tinkerbell Workflow. diff --git a/tink/internal/client_test/common_test.go b/tink/client_test/common_test.go similarity index 100% rename from tink/internal/client_test/common_test.go rename to tink/client_test/common_test.go diff --git a/tink/internal/client_test/hardware_test.go b/tink/client_test/hardware_test.go similarity index 98% rename from tink/internal/client_test/hardware_test.go rename to tink/client_test/hardware_test.go index 6c6d4302..de7f2f0a 100644 --- a/tink/internal/client_test/hardware_test.go +++ b/tink/client_test/hardware_test.go @@ -21,7 +21,7 @@ import ( "testing" . "github.com/onsi/gomega" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" ) func TestHardwareLifecycle(t *testing.T) { diff --git a/tink/internal/client_test/template_test.go b/tink/client_test/template_test.go similarity index 97% rename from tink/internal/client_test/template_test.go rename to tink/client_test/template_test.go index 793faa42..8f92139f 100644 --- a/tink/internal/client_test/template_test.go +++ b/tink/client_test/template_test.go @@ -21,7 +21,7 @@ import ( "testing" . "github.com/onsi/gomega" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "k8s.io/apimachinery/pkg/util/rand" ) diff --git a/tink/internal/client_test/workflow_test.go b/tink/client_test/workflow_test.go similarity index 96% rename from tink/internal/client_test/workflow_test.go rename to tink/client_test/workflow_test.go index 8210b3ec..ad4293dd 100644 --- a/tink/internal/client_test/workflow_test.go +++ b/tink/client_test/workflow_test.go @@ -21,7 +21,7 @@ import ( "testing" . "github.com/onsi/gomega" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "k8s.io/apimachinery/pkg/util/rand" ) diff --git a/tink/internal/controllers/common/common.go b/tink/controllers/common/common.go similarity index 100% rename from tink/internal/controllers/common/common.go rename to tink/controllers/common/common.go diff --git a/tink/internal/controllers/common_test/common_test.go b/tink/controllers/common_test/common_test.go similarity index 97% rename from tink/internal/controllers/common_test/common_test.go rename to tink/controllers/common_test/common_test.go index f55dd16f..961984e9 100644 --- a/tink/internal/controllers/common_test/common_test.go +++ b/tink/controllers/common_test/common_test.go @@ -22,7 +22,7 @@ import ( . "github.com/onsi/gomega" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/tink/internal/controllers/hardware/controller.go b/tink/controllers/hardware/controller.go similarity index 90% rename from tink/internal/controllers/hardware/controller.go rename to tink/controllers/hardware/controller.go index cdfa8467..50aa8f05 100644 --- a/tink/internal/controllers/hardware/controller.go +++ b/tink/controllers/hardware/controller.go @@ -28,6 +28,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" ) type hardwareClient interface { @@ -46,9 +49,13 @@ type Reconciler struct { } // SetupWithManager configures reconciler with a given manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, hwChan <-chan event.GenericEvent) error { return ctrl.NewControllerManagedBy(mgr). For(&tinkv1alpha1.Hardware{}). + Watches( + &source.Channel{Source: hwChan}, + &handler.EnqueueRequestForObject{}, + ). Complete(r) } diff --git a/tink/internal/controllers/template/controller.go b/tink/controllers/template/controller.go similarity index 93% rename from tink/internal/controllers/template/controller.go rename to tink/controllers/template/controller.go index 1c080918..b406f190 100644 --- a/tink/internal/controllers/template/controller.go +++ b/tink/controllers/template/controller.go @@ -24,8 +24,8 @@ import ( "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" - tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" + tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/common" "github.com/tinkerbell/tink/protos/template" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -33,6 +33,9 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" ) type templateClient interface { @@ -51,9 +54,13 @@ type Reconciler struct { } // SetupWithManager configures reconciler with a given manager. -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, templateChan <-chan event.GenericEvent) error { return ctrl.NewControllerManagedBy(mgr). For(&tinkv1alpha1.Template{}). + Watches( + &source.Channel{Source: templateChan}, + &handler.EnqueueRequestForObject{}, + ). Complete(r) } diff --git a/tink/internal/controllers/template/controller_internal_test.go b/tink/controllers/template/controller_internal_test.go similarity index 99% rename from tink/internal/controllers/template/controller_internal_test.go rename to tink/controllers/template/controller_internal_test.go index 126ab9ce..d37bc8bc 100644 --- a/tink/internal/controllers/template/controller_internal_test.go +++ b/tink/controllers/template/controller_internal_test.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/gomega" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" - tinkfake "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client/fake" + tinkfake "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client/fake" "github.com/tinkerbell/tink/protos/template" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/tink/internal/controllers/workflow/controller.go b/tink/controllers/workflow/controller.go similarity index 83% rename from tink/internal/controllers/workflow/controller.go rename to tink/controllers/workflow/controller.go index ae1ff441..9bd5b6df 100644 --- a/tink/internal/controllers/workflow/controller.go +++ b/tink/controllers/workflow/controller.go @@ -24,14 +24,17 @@ import ( "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" - tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/client" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/internal/controllers/common" + tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/common" "github.com/tinkerbell/tink/protos/workflow" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" ) type workflowClient interface { @@ -40,8 +43,8 @@ type workflowClient interface { Delete(ctx context.Context, id string) error } -// WorkflowReconciler implements Reconciler interface by managing Tinkerbell workflows. -type WorkflowReconciler struct { +// Reconciler implements Reconciler interface by managing Tinkerbell workflows. +type Reconciler struct { client.Client WorkflowClient workflowClient Log logr.Logger @@ -49,16 +52,20 @@ type WorkflowReconciler struct { } // SetupWithManager configures reconciler with a given manager. -func (r *WorkflowReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, workflowChan <-chan event.GenericEvent) error { return ctrl.NewControllerManagedBy(mgr). For(&tinkv1alpha1.Workflow{}). + Watches( + &source.Channel{Source: workflowChan}, + &handler.EnqueueRequestForObject{}, + ). Complete(r) } // +kubebuilder:rbac:groups=tinkerbell.org,resources=workflows;workflows/status,verbs=get;list;watch;create;update;patch;delete // Reconcile ensures state of Tinkerbell workflows. -func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { +func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() logger := r.Log.WithValues("workflow", req.NamespacedName.Name) @@ -87,7 +94,7 @@ func (r *WorkflowReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return r.reconcileNormal(ctx, workflow) } -func (r *WorkflowReconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { +func (r *Reconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { logger := r.Log.WithValues("workflow", w.Name) var workflowID string @@ -117,7 +124,7 @@ func (r *WorkflowReconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha return ctrl.Result{}, nil } -func (r *WorkflowReconciler) createWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (string, error) { +func (r *Reconciler) createWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (string, error) { logger := r.Log.WithValues("workflow", w.Name) hw := &tinkv1alpha1.Hardware{} @@ -151,7 +158,7 @@ func (r *WorkflowReconciler) createWorkflow(ctx context.Context, w *tinkv1alpha1 return id, nil } -func (r *WorkflowReconciler) getWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (*workflow.Workflow, error) { +func (r *Reconciler) getWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (*workflow.Workflow, error) { annotations := w.GetAnnotations() if annotations == nil { annotations = map[string]string{} @@ -160,7 +167,7 @@ func (r *WorkflowReconciler) getWorkflow(ctx context.Context, w *tinkv1alpha1.Wo return r.WorkflowClient.Get(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation]) } -func (r *WorkflowReconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { +func (r *Reconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { // Create a patch for use later patch := client.MergeFrom(w.DeepCopy()) From 303da8cb9b856d7077fc7c95b057aa9b0c51126c Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Mon, 21 Dec 2020 17:42:31 -0500 Subject: [PATCH 10/15] update generated code --- config/crd/bases/tinkerbell.org_hardware.yaml | 8 ++++++-- config/crd/bases/tinkerbell.org_templates.yaml | 8 ++++++-- config/crd/bases/tinkerbell.org_workflows.yaml | 11 ++++++++--- tink/api/v1alpha1/zz_generated.deepcopy.go | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/config/crd/bases/tinkerbell.org_hardware.yaml b/config/crd/bases/tinkerbell.org_hardware.yaml index 752a01d7..3cdfcbe6 100644 --- a/config/crd/bases/tinkerbell.org_hardware.yaml +++ b/config/crd/bases/tinkerbell.org_hardware.yaml @@ -24,10 +24,14 @@ spec: description: Hardware is the Schema for the Hardware API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object diff --git a/config/crd/bases/tinkerbell.org_templates.yaml b/config/crd/bases/tinkerbell.org_templates.yaml index 8968bfdc..7017ea97 100644 --- a/config/crd/bases/tinkerbell.org_templates.yaml +++ b/config/crd/bases/tinkerbell.org_templates.yaml @@ -24,10 +24,14 @@ spec: description: Template is the Schema for the Templates API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object diff --git a/config/crd/bases/tinkerbell.org_workflows.yaml b/config/crd/bases/tinkerbell.org_workflows.yaml index 2dd5ca01..606a5ae5 100644 --- a/config/crd/bases/tinkerbell.org_workflows.yaml +++ b/config/crd/bases/tinkerbell.org_workflows.yaml @@ -24,10 +24,14 @@ spec: description: Workflow is the Schema for the Workflows API. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -38,7 +42,8 @@ spec: description: Name of the Hardware associated with this workflow. type: string hardwareTemplate: - description: HardwareTemplate is the template used for creating the workflow. + description: HardwareTemplate is the template used for creating the + workflow. type: string templateRef: description: Name of the Template associated with this workflow. diff --git a/tink/api/v1alpha1/zz_generated.deepcopy.go b/tink/api/v1alpha1/zz_generated.deepcopy.go index 0cbf9f19..313a9ee0 100644 --- a/tink/api/v1alpha1/zz_generated.deepcopy.go +++ b/tink/api/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ // +build !ignore_autogenerated /* -Copyright 2020 The Kubernetes Authors. +Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 1746f694a8fc780e325b4c36478040a7ada8267d Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Tue, 22 Dec 2020 17:27:20 -0500 Subject: [PATCH 11/15] Add tests for tink informers, update statuses --- config/crd/bases/tinkerbell.org_hardware.yaml | 72 ++++ main.go | 153 +------ tink/api/v1alpha1/hardware_types.go | 66 ++++ tink/api/v1alpha1/template_types.go | 6 + tink/api/v1alpha1/zz_generated.deepcopy.go | 144 ++++++- tink/client_test/common_test.go | 166 -------- tink/client_test/hardware_test.go | 7 +- tink/client_test/template_test.go | 9 +- tink/client_test/workflow_test.go | 5 +- tink/controllers/hardware/controller.go | 84 ++++ tink/controllers/template/controller.go | 16 + tink/controllers/workflow/controller.go | 21 + tink/sources/tink_events.go | 198 ++++++++++ tink/sources/tink_events_internal_test.go | 213 ++++++++++ tink/sources_test/tink_events_test.go | 374 ++++++++++++++++++ tink/test/utils/generators.go | 189 +++++++++ 16 files changed, 1407 insertions(+), 316 deletions(-) create mode 100644 tink/sources/tink_events.go create mode 100644 tink/sources/tink_events_internal_test.go create mode 100644 tink/sources_test/tink_events_test.go create mode 100644 tink/test/utils/generators.go diff --git a/config/crd/bases/tinkerbell.org_hardware.yaml b/config/crd/bases/tinkerbell.org_hardware.yaml index 3cdfcbe6..ebcdfd58 100644 --- a/config/crd/bases/tinkerbell.org_hardware.yaml +++ b/config/crd/bases/tinkerbell.org_hardware.yaml @@ -47,6 +47,78 @@ spec: type: object status: description: HardwareStatus defines the observed state of Hardware. + properties: + state: + type: string + tinkInterfaces: + items: + properties: + dhcp: + properties: + arch: + type: string + hostname: + type: string + iface_name: + type: string + ip: + properties: + address: + type: string + family: + format: int64 + type: integer + gateway: + type: string + netmask: + type: string + type: object + lease_time: + format: int64 + type: integer + mac: + type: string + name_servers: + items: + type: string + type: array + time_servers: + items: + type: string + type: array + uefi: + type: boolean + type: object + netboot: + properties: + allowPXE: + type: boolean + allowWorkflow: + type: boolean + ipxe: + properties: + contents: + type: string + url: + type: string + type: object + osie: + properties: + baseURL: + type: string + initrd: + type: string + kernel: + type: string + type: object + type: object + type: object + type: array + tinkMetadata: + type: string + tinkVersion: + format: int64 + type: integer type: object type: object served: true diff --git a/main.go b/main.go index d9775bac..1425a50b 100644 --- a/main.go +++ b/main.go @@ -16,32 +16,25 @@ limitations under the License. package main import ( - "context" "errors" "flag" - "fmt" - "io" "os" "time" - "github.com/go-logr/logr" "github.com/tinkerbell/cluster-api-provider-tinkerbell/controllers" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" tinkhardware "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/hardware" tinktemplate "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/template" tinkworkflow "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/workflow" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/sources" tinkclient "github.com/tinkerbell/tink/client" - tinkinformers "github.com/tinkerbell/tink/client/informers" tinkevents "github.com/tinkerbell/tink/protos/events" - "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/client-go/tools/record" "k8s.io/klog" "k8s.io/klog/klogr" ctrl "sigs.k8s.io/controller-runtime" - ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -168,31 +161,28 @@ func main() { templateChan := make(chan event.GenericEvent) workflowChan := make(chan event.GenericEvent) - if err := mgr.Add(&tinkEventWatcher{ - k8sClient: mgr.GetClient(), - eventCh: hwChan, - logger: ctrl.Log.WithName("tinkwatcher").WithName("Hardware"), - resourceType: tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE, + if err := mgr.Add(&sources.TinkEventWatcher{ + EventCh: hwChan, + Logger: ctrl.Log.WithName("tinkwatcher").WithName("Hardware"), + ResourceType: tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE, }); err != nil { setupLog.Error(err, "unable to create tink watcher", "tinkwatcher", "Hardware") os.Exit(1) } - if err := mgr.Add(&tinkEventWatcher{ - k8sClient: mgr.GetClient(), - eventCh: templateChan, - logger: ctrl.Log.WithName("tinkwatcher").WithName("Template"), - resourceType: tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE, + if err := mgr.Add(&sources.TinkEventWatcher{ + EventCh: templateChan, + Logger: ctrl.Log.WithName("tinkwatcher").WithName("Template"), + ResourceType: tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE, }); err != nil { setupLog.Error(err, "unable to create tink watcher", "tinkwatcher", "Template") os.Exit(1) } - if err := mgr.Add(&tinkEventWatcher{ - k8sClient: mgr.GetClient(), - eventCh: workflowChan, - logger: ctrl.Log.WithName("tinkwatcher").WithName("Workflow"), - resourceType: tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW, + if err := mgr.Add(&sources.TinkEventWatcher{ + EventCh: workflowChan, + Logger: ctrl.Log.WithName("tinkwatcher").WithName("Workflow"), + ResourceType: tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW, }); err != nil { setupLog.Error(err, "unable to create tink watcher", "tinkwatcher", "Workflow") os.Exit(1) @@ -271,120 +261,3 @@ func main() { os.Exit(1) } } - -type tinkEventWatcher struct { - k8sClient ctrlclient.Client - eventCh chan<- event.GenericEvent - logger logr.Logger - resourceType tinkevents.ResourceType -} - -func (w *tinkEventWatcher) getHardwareForID(ctx context.Context, id string) (*tinkv1.Hardware, error) { - hwList := &tinkv1.HardwareList{} - if err := w.k8sClient.List(ctx, hwList); err != nil { - return nil, err - } - - for i, h := range hwList.Items { - if h.TinkID() == id { - return &hwList.Items[i], nil - } - } - - return nil, nil -} - -func (w *tinkEventWatcher) getTemplateForID(ctx context.Context, id string) (*tinkv1.Template, error) { - templateList := &tinkv1.TemplateList{} - if err := w.k8sClient.List(ctx, templateList); err != nil { - return nil, err - } - - for i, t := range templateList.Items { - if t.TinkID() == id { - return &templateList.Items[i], nil - } - } - - return nil, nil -} - -func (w *tinkEventWatcher) getWorkflowForID(ctx context.Context, id string) (*tinkv1.Workflow, error) { - workflowList := &tinkv1.WorkflowList{} - if err := w.k8sClient.List(ctx, workflowList); err != nil { - return nil, err - } - - for i, w := range workflowList.Items { - if w.TinkID() == id { - return &workflowList.Items[i], nil - } - } - - return nil, nil -} - -func (w *tinkEventWatcher) generateEventForTinkID(ctx context.Context, id string) error { - switch w.resourceType { - case tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE: - hw, _ := w.getHardwareForID(ctx, id) - w.eventCh <- event.GenericEvent{ - Meta: hw, - Object: hw, - } - case tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE: - template, _ := w.getTemplateForID(ctx, id) - w.eventCh <- event.GenericEvent{ - Meta: template, - Object: template, - } - case tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW: - workflow, _ := w.getWorkflowForID(ctx, id) - w.eventCh <- event.GenericEvent{ - Meta: workflow, - Object: workflow, - } - default: - return fmt.Errorf("unknown resource type: %s", w.resourceType.String()) - } - - return nil -} - -func (w *tinkEventWatcher) Start(stopCh <-chan struct{}) error { - now := time.Now() - - req := &tinkevents.WatchRequest{ - EventTypes: []tinkevents.EventType{ - tinkevents.EventType_EVENT_TYPE_CREATED, - tinkevents.EventType_EVENT_TYPE_UPDATED, - tinkevents.EventType_EVENT_TYPE_DELETED, - }, - ResourceTypes: []tinkevents.ResourceType{w.resourceType}, - WatchEventsFrom: timestamppb.New(now), - } - - ctx, cancel := context.WithCancel(context.Background()) - - go func() { - <-stopCh - cancel() - }() - - tinkInformer := tinkinformers.New() - - w.logger.Info("Starting Tinkerbell Informer", "resourceType", w.resourceType.String()) - - err := tinkInformer.Start(ctx, req, func(e *tinkevents.Event) error { - if err := w.generateEventForTinkID(ctx, e.GetResourceId()); err != nil { - return err - } - - return nil - }) - if err != nil && !errors.Is(err, io.EOF) { - return err - } - - return nil -} diff --git a/tink/api/v1alpha1/hardware_types.go b/tink/api/v1alpha1/hardware_types.go index 6b2f30fc..c011d6ec 100644 --- a/tink/api/v1alpha1/hardware_types.go +++ b/tink/api/v1alpha1/hardware_types.go @@ -20,6 +20,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type HardwareState string + +const ( + HardwareError = HardwareState("Error") + HardwareReady = HardwareState("Ready") +) + // HardwareSpec defines the desired state of Hardware. type HardwareSpec struct { // ID is the ID of the hardware in Tinkerbell @@ -29,6 +36,65 @@ type HardwareSpec struct { // HardwareStatus defines the observed state of Hardware. type HardwareStatus struct { + TinkMetadata string `json:"tinkMetadata,omitempty"` + + TinkVersion int64 `json:"tinkVersion,omitempty"` + + TinkInterfaces []Interface `json:"tinkInterfaces,omitempty"` + + State HardwareState `json:"state,omitempty"` +} + +type Interface struct { + //+optional + Netboot *Netboot `json:"netboot,omitempty"` + + //+optional + DHCP *DHCP `json:"dhcp,omitempty"` +} + +type Netboot struct { + //+optional + AllowPXE *bool `json:"allowPXE,omitempty"` + + //+optional + AllowWorkflow *bool `json:"allowWorkflow,omitempty"` + + //+optional + IPXE *IPXE `json:"ipxe,omitempty"` + + //+optional + OSIE *OSIE `json:"osie,omitempty"` +} + +type IPXE struct { + URL string `json:"url,omitempty"` + Contents string `json:"contents,omitempty"` +} + +type OSIE struct { + BaseURL string `json:"baseURL,omitempty"` + Kernel string `json:"kernel,omitempty"` + Initrd string `json:"initrd,omitempty"` +} + +type DHCP struct { + MAC string `json:"mac,omitempty"` + Hostname string `json:"hostname,omitempty"` + LeaseTime int64 `json:"lease_time,omitempty"` + NameServers []string `json:"name_servers,omitempty"` + TimeServers []string `json:"time_servers,omitempty"` + Arch string `json:"arch,omitempty"` + UEFI bool `json:"uefi,omitempty"` + IfaceName string `json:"iface_name,omitempty"` + IP *IP `json:"ip,omitempty"` +} + +type IP struct { + Address string `json:"address,omitempty"` + Netmask string `json:"netmask,omitempty"` + Gateway string `json:"gateway,omitempty"` + Family int64 `json:"family,omitempty"` } // +kubebuilder:subresource:status diff --git a/tink/api/v1alpha1/template_types.go b/tink/api/v1alpha1/template_types.go index 3c92631a..819f1038 100644 --- a/tink/api/v1alpha1/template_types.go +++ b/tink/api/v1alpha1/template_types.go @@ -20,7 +20,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +type TemplateState string + const ( + TemplateError = TemplateState("Error") + TemplateReady = TemplateState("Ready") + // TemplateIDAnnotation is used by the controller to store the // ID assigned to the template by Tinkerbell. TemplateIDAnnotation = "template.tinkerbell.org/id" @@ -36,6 +41,7 @@ type TemplateSpec struct { // TemplateStatus defines the observed state of Template. type TemplateStatus struct { + State TemplateState `json:"state,omitempty"` } // +kubebuilder:subresource:status diff --git a/tink/api/v1alpha1/zz_generated.deepcopy.go b/tink/api/v1alpha1/zz_generated.deepcopy.go index 313a9ee0..afedebfe 100644 --- a/tink/api/v1alpha1/zz_generated.deepcopy.go +++ b/tink/api/v1alpha1/zz_generated.deepcopy.go @@ -24,13 +24,43 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DHCP) DeepCopyInto(out *DHCP) { + *out = *in + if in.NameServers != nil { + in, out := &in.NameServers, &out.NameServers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TimeServers != nil { + in, out := &in.TimeServers, &out.TimeServers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IP != nil { + in, out := &in.IP, &out.IP + *out = new(IP) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DHCP. +func (in *DHCP) DeepCopy() *DHCP { + if in == nil { + return nil + } + out := new(DHCP) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Hardware) DeepCopyInto(out *Hardware) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hardware. @@ -101,6 +131,13 @@ func (in *HardwareSpec) DeepCopy() *HardwareSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HardwareStatus) DeepCopyInto(out *HardwareStatus) { *out = *in + if in.TinkInterfaces != nil { + in, out := &in.TinkInterfaces, &out.TinkInterfaces + *out = make([]Interface, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HardwareStatus. @@ -113,6 +150,111 @@ func (in *HardwareStatus) DeepCopy() *HardwareStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IP) DeepCopyInto(out *IP) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IP. +func (in *IP) DeepCopy() *IP { + if in == nil { + return nil + } + out := new(IP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPXE) DeepCopyInto(out *IPXE) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPXE. +func (in *IPXE) DeepCopy() *IPXE { + if in == nil { + return nil + } + out := new(IPXE) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Interface) DeepCopyInto(out *Interface) { + *out = *in + if in.Netboot != nil { + in, out := &in.Netboot, &out.Netboot + *out = new(Netboot) + (*in).DeepCopyInto(*out) + } + if in.DHCP != nil { + in, out := &in.DHCP, &out.DHCP + *out = new(DHCP) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Interface. +func (in *Interface) DeepCopy() *Interface { + if in == nil { + return nil + } + out := new(Interface) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Netboot) DeepCopyInto(out *Netboot) { + *out = *in + if in.AllowPXE != nil { + in, out := &in.AllowPXE, &out.AllowPXE + *out = new(bool) + **out = **in + } + if in.AllowWorkflow != nil { + in, out := &in.AllowWorkflow, &out.AllowWorkflow + *out = new(bool) + **out = **in + } + if in.IPXE != nil { + in, out := &in.IPXE, &out.IPXE + *out = new(IPXE) + **out = **in + } + if in.OSIE != nil { + in, out := &in.OSIE, &out.OSIE + *out = new(OSIE) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Netboot. +func (in *Netboot) DeepCopy() *Netboot { + if in == nil { + return nil + } + out := new(Netboot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OSIE) DeepCopyInto(out *OSIE) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OSIE. +func (in *OSIE) DeepCopy() *OSIE { + if in == nil { + return nil + } + out := new(OSIE) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Template) DeepCopyInto(out *Template) { *out = *in diff --git a/tink/client_test/common_test.go b/tink/client_test/common_test.go index 14bec0b0..a3098738 100644 --- a/tink/client_test/common_test.go +++ b/tink/client_test/common_test.go @@ -17,15 +17,10 @@ limitations under the License. package client_test import ( - "crypto/rand" "crypto/x509" - "fmt" "io/ioutil" - "math/big" - "net" "net/http" "os" - "sync" "testing" . "github.com/onsi/gomega" @@ -36,167 +31,6 @@ import ( "google.golang.org/grpc/credentials" ) -const helloWorldTemplate = `version: "0.1" -name: hello_world_workflow -global_timeout: 600 -tasks: - - name: "hello world" - worker: "{{.device_1}}" - actions: - - name: "hello_world" - image: hello-world - timeout: 60` - -// These are CIDRs that we should not come across in a real -// environment, since they are reserved for use in documentation -// and examples. -var testCIDRs = [...]string{ - "192.0.2.0/24", - "198.51.100.0/24", - "203.0.113.0/24", -} - -var IPGetter = ipGetter{ - addresses: make(map[string]string), -} - -type ipGetter struct { - addresses map[string]string - lock sync.Mutex -} - -func (i *ipGetter) nextAddressFromCIDR(cidr string) (string, string, string, error) { - i.lock.Lock() - defer i.lock.Unlock() - - _, network, err := net.ParseCIDR(cidr) - if err != nil { - return "", "", "", fmt.Errorf("failed to parse cidr: %w", err) - } - - netMask := net.IP(network.Mask).String() - - // Use the first available address as the gateway address - gw := make(net.IP, len(network.IP)) - copy(gw, network.IP) - gw[len(gw)-1]++ - gateway := gw.String() - - // Attempt to get the last address used, othewise use the - // gateway address as the starting point - lastAddress, ok := i.addresses[cidr] - if !ok { - lastAddress = gateway - } - - // Get the next IP by incrementing lastAddress - nextIP := net.ParseIP(lastAddress) - nextIP[len(nextIP)-1]++ - - ip := nextIP.String() - - // Store the last address - i.addresses[cidr] = ip - - return ip, netMask, gateway, nil -} - -var MACGenerator = macGenerator{ - addresses: make(map[string]struct{}), -} - -type macGenerator struct { - addresses map[string]struct{} - lock sync.Mutex -} - -func (m *macGenerator) Get() (string, error) { - m.lock.Lock() - defer m.lock.Unlock() - - for { - mac := net.HardwareAddr(make([]byte, 6)) - - _, err := rand.Read(mac) - if err != nil { - return "", fmt.Errorf("failed to generate random mac: %w", err) - } - - // Ensure the individual bit is set - mac[0] &= ^byte(1) - - // Ensure the local bit is set - mac[0] |= byte(2) - - key := mac.String() - if _, found := m.addresses[key]; !found { - m.addresses[key] = struct{}{} - - return key, nil - } - } -} - -func generateTemplate(name, data string) *template.WorkflowTemplate { - return &template.WorkflowTemplate{ - Name: name, - Data: data, - } -} - -func generateHardware(numInterfaces int) (*hardware.Hardware, error) { - hw := &hardware.Hardware{ - Network: &hardware.Hardware_Network{}, - } - - for i := 0; i < numInterfaces; i++ { - cidr := testCIDRs[i%len(testCIDRs)] - - ni, err := generateHardwareInterface(cidr) - if err != nil { - return nil, err - } - - hw.Network.Interfaces = append(hw.Network.Interfaces, ni) - } - - return hw, nil -} - -func generateHardwareInterface(cidr string) (*hardware.Hardware_Network_Interface, error) { - if cidr == "" { - i, err := rand.Int(rand.Reader, big.NewInt(int64(len(testCIDRs)))) - if err != nil { - return nil, fmt.Errorf("failed to get random index for cidr: %w", err) - } - - cidr = testCIDRs[i.Int64()] - } - - ip, netmask, gateway, err := IPGetter.nextAddressFromCIDR(cidr) - if err != nil { - return nil, err - } - - mac, err := MACGenerator.Get() - if err != nil { - return nil, err - } - - ni := &hardware.Hardware_Network_Interface{ - Dhcp: &hardware.Hardware_DHCP{ - Mac: mac, - Ip: &hardware.Hardware_DHCP_IP{ - Address: ip, - Netmask: netmask, - Gateway: gateway, - }, - }, - } - - return ni, nil -} - func realConn(t *testing.T) *grpc.ClientConn { g := NewWithT(t) diff --git a/tink/client_test/hardware_test.go b/tink/client_test/hardware_test.go index de7f2f0a..853a9f97 100644 --- a/tink/client_test/hardware_test.go +++ b/tink/client_test/hardware_test.go @@ -22,15 +22,16 @@ import ( . "github.com/onsi/gomega" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + testutils "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/test/utils" ) -func TestHardwareLifecycle(t *testing.T) { +func TestHardwareLifecycle(t *testing.T) { //nolint:funlen g := NewWithT(t) ctx := context.Background() hardwareClient := client.NewHardwareClient(realHardwareClient(t)) // Create a Hardware resource in Tinkerbell - testHardware, err := generateHardware(2) + testHardware, err := testutils.GenerateHardware(2) g.Expect(err).NotTo(HaveOccurred()) hardwareMACs := make([]string, 0, len(testHardware.Network.Interfaces)) @@ -93,7 +94,7 @@ func TestHardwareLifecycle(t *testing.T) { } // Ensure that we can update the hardware in Tinkerbell - additionalInterface, err := generateHardwareInterface("") + additionalInterface, err := testutils.GenerateHardwareInterface("") g.Expect(err).NotTo(HaveOccurred()) testHardware.Network.Interfaces = append(testHardware.Network.Interfaces, additionalInterface) diff --git a/tink/client_test/template_test.go b/tink/client_test/template_test.go index 8f92139f..fb86f429 100644 --- a/tink/client_test/template_test.go +++ b/tink/client_test/template_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/gomega" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + testutils "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/test/utils" "k8s.io/apimachinery/pkg/util/rand" ) @@ -56,7 +57,7 @@ func TestTemplateLifecycle(t *testing.T) { g.Expect(err).To(MatchError(client.ErrNotFound)) // Create a template using the hello world template - testTemplate := generateTemplate(name, helloWorldTemplate) + testTemplate := testutils.GenerateTemplate(name, testutils.HelloWorldTemplate) g.Expect(templateClient.Create(ctx, testTemplate)).To(Succeed()) // Ensure that the template now has an ID set @@ -74,7 +75,7 @@ func TestTemplateLifecycle(t *testing.T) { }() // Verify that trying to create a template with a duplicate name fails - testDuplicateName := generateTemplate(name, sampleTemplate) + testDuplicateName := testutils.GenerateTemplate(name, sampleTemplate) g.Expect(templateClient.Create(ctx, testDuplicateName)).NotTo(Succeed()) // Ensure we can get the template we just created by ID @@ -84,7 +85,7 @@ func TestTemplateLifecycle(t *testing.T) { g.Expect(resByID).NotTo(BeNil()) g.Expect(resByID.Id).To(BeEquivalentTo(expectedID)) g.Expect(resByID.Name).To(BeEquivalentTo(name)) - g.Expect(resByID.Data).To(BeEquivalentTo(helloWorldTemplate)) + g.Expect(resByID.Data).To(BeEquivalentTo(testutils.HelloWorldTemplate)) // Ensure we can get the previously created template by Name // and it has the values we expect @@ -93,7 +94,7 @@ func TestTemplateLifecycle(t *testing.T) { g.Expect(resByName).NotTo(BeNil()) g.Expect(resByName.Id).To(BeEquivalentTo(expectedID)) g.Expect(resByName.Name).To(BeEquivalentTo(name)) - g.Expect(resByName.Data).To(BeEquivalentTo(helloWorldTemplate)) + g.Expect(resByName.Data).To(BeEquivalentTo(testutils.HelloWorldTemplate)) // Update the template's data testTemplate.Data = sampleTemplate diff --git a/tink/client_test/workflow_test.go b/tink/client_test/workflow_test.go index ad4293dd..137c8692 100644 --- a/tink/client_test/workflow_test.go +++ b/tink/client_test/workflow_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/gomega" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + testutils "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/test/utils" "k8s.io/apimachinery/pkg/util/rand" ) @@ -34,7 +35,7 @@ func TestWorkflowLifecycle(t *testing.T) { workflowClient := client.NewWorkflowClient(realWorkflowClient(t), hardwareClient) // Create a template for the workflow to use - testTemplate := generateTemplate(rand.String(12), helloWorldTemplate) + testTemplate := testutils.GenerateTemplate(rand.String(12), testutils.HelloWorldTemplate) g.Expect(templateClient.Create(ctx, testTemplate)).To(Succeed()) // Attempt to cleanup even if later assertions fail @@ -43,7 +44,7 @@ func TestWorkflowLifecycle(t *testing.T) { }() // Create hardware for the workflow to use - testHardware, err := generateHardware(3) + testHardware, err := testutils.GenerateHardware(3) g.Expect(err).NotTo(HaveOccurred()) g.Expect(hardwareClient.Create(ctx, testHardware)).To(Succeed()) diff --git a/tink/controllers/hardware/controller.go b/tink/controllers/hardware/controller.go index 50aa8f05..2d5bba38 100644 --- a/tink/controllers/hardware/controller.go +++ b/tink/controllers/hardware/controller.go @@ -22,10 +22,12 @@ import ( "fmt" "github.com/go-logr/logr" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" "github.com/tinkerbell/tink/protos/hardware" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" @@ -91,6 +93,21 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, h *tinkv1alpha1.Hardwa tinkHardware, err := r.HardwareClient.Get(ctx, h.Spec.ID, "", "") if err != nil { + if apierrors.IsNotFound(err) { + // Mark the hardware as being in an error state if it's not present in Tinkerbell + patch := client.MergeFrom(h.DeepCopy()) + + h.Status.State = tinkv1alpha1.HardwareError + + if err := r.Client.Patch(ctx, h, patch); err != nil { + logger.Error(err, "Failed to patch hardware") + + return ctrl.Result{}, fmt.Errorf("failed to patch hardware: %w", err) + } + + return ctrl.Result{}, nil + } + logger.Error(err, "Failed to get hardware from Tinkerbell") return ctrl.Result{}, fmt.Errorf("failed to get hardware from Tinkerbell: %w", err) @@ -98,5 +115,72 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, h *tinkv1alpha1.Hardwa logger.Info("Found hardware in tinkerbell", "tinkHardware", tinkHardware) + return r.reconcileStatus(ctx, h, tinkHardware) +} + +func (r *Reconciler) reconcileStatus(ctx context.Context, h *tinkv1alpha1.Hardware, tinkHardware *hardware.Hardware) (ctrl.Result, error) { + logger := r.Log.WithValues("hardware", h.Name) + patch := client.MergeFrom(h.DeepCopy()) + + h.Status.TinkMetadata = tinkHardware.GetMetadata() + h.Status.TinkVersion = tinkHardware.GetVersion() + h.Status.TinkInterfaces = []v1alpha1.Interface{} + + for _, iface := range tinkHardware.GetNetwork().GetInterfaces() { + tinkInterface := v1alpha1.Interface{} + if netboot := iface.GetNetboot(); netboot != nil { + tinkInterface.Netboot = &v1alpha1.Netboot{ + AllowPXE: pointer.BoolPtr(netboot.GetAllowPxe()), + AllowWorkflow: pointer.BoolPtr(netboot.GetAllowWorkflow()), + } + if ipxe := netboot.GetIpxe(); ipxe != nil { + tinkInterface.Netboot.IPXE = &v1alpha1.IPXE{ + URL: ipxe.GetUrl(), + Contents: ipxe.GetContents(), + } + } + + if osie := netboot.GetOsie(); osie != nil { + tinkInterface.Netboot.OSIE = &v1alpha1.OSIE{ + BaseURL: osie.GetBaseUrl(), + Kernel: osie.GetKernel(), + Initrd: osie.GetInitrd(), + } + } + } + + if dhcp := iface.GetDhcp(); dhcp != nil { + tinkInterface.DHCP = &v1alpha1.DHCP{ + MAC: dhcp.GetMac(), + Hostname: dhcp.GetHostname(), + LeaseTime: dhcp.GetLeaseTime(), + NameServers: dhcp.GetNameServers(), + TimeServers: dhcp.GetTimeServers(), + Arch: dhcp.GetArch(), + UEFI: dhcp.GetUefi(), + IfaceName: dhcp.GetIfaceName(), + } + + if ip := dhcp.GetIp(); ip != nil { + tinkInterface.DHCP.IP = &v1alpha1.IP{ + Address: ip.GetAddress(), + Netmask: ip.GetNetmask(), + Gateway: ip.GetGateway(), + Family: ip.GetFamily(), + } + } + } + + h.Status.TinkInterfaces = append(h.Status.TinkInterfaces, tinkInterface) + } + + h.Status.State = v1alpha1.HardwareReady + + if err := r.Client.Patch(ctx, h, patch); err != nil { + logger.Error(err, "Failed to patch hardware") + + return ctrl.Result{}, fmt.Errorf("failed to patch hardware: %w", err) + } + return ctrl.Result{}, nil } diff --git a/tink/controllers/template/controller.go b/tink/controllers/template/controller.go index b406f190..22856de1 100644 --- a/tink/controllers/template/controller.go +++ b/tink/controllers/template/controller.go @@ -23,6 +23,7 @@ import ( "fmt" "github.com/go-logr/logr" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/common" @@ -144,6 +145,21 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Templa return ctrl.Result{}, fmt.Errorf("failed to patch template: %w", err) } + return r.reconcileStatus(ctx, t, tinkTemplate) +} + +func (r *Reconciler) reconcileStatus(ctx context.Context, t *tinkv1alpha1.Template, tinkTemplate *template.WorkflowTemplate) (ctrl.Result, error) { + logger := r.Log.WithValues("template", t.Name) + patch := client.MergeFrom(t.DeepCopy()) + + t.Status.State = v1alpha1.TemplateReady + + if err := r.Client.Patch(ctx, t, patch); err != nil { + logger.Error(err, "Failed to patch template") + + return ctrl.Result{}, fmt.Errorf("failed to patch template: %w", err) + } + return ctrl.Result{}, nil } diff --git a/tink/controllers/workflow/controller.go b/tink/controllers/workflow/controller.go index 9bd5b6df..1b50b5e1 100644 --- a/tink/controllers/workflow/controller.go +++ b/tink/controllers/workflow/controller.go @@ -109,6 +109,11 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workfl } workflowID = id + + tinkWorkflow, err = r.getWorkflow(ctx, w) + if err != nil { + return ctrl.Result{}, err + } case err != nil: return ctrl.Result{}, err @@ -121,6 +126,22 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workfl return ctrl.Result{}, fmt.Errorf("failed to ensure id annotation on workflow: %w", err) } + return r.reconcileStatus(ctx, w, tinkWorkflow) +} + +func (r *Reconciler) reconcileStatus(ctx context.Context, w *tinkv1alpha1.Workflow, tinkWorkflow *workflow.Workflow) (ctrl.Result, error) { + logger := r.Log.WithValues("workflow", w.Name) + patch := client.MergeFrom(w.DeepCopy()) + + w.Status.Data = tinkWorkflow.GetData() + w.Status.State = tinkWorkflow.GetState().String() + + if err := r.Client.Patch(ctx, w, patch); err != nil { + logger.Error(err, "Failed to patch workflow") + + return ctrl.Result{}, fmt.Errorf("failed to patch workflow: %w", err) + } + return ctrl.Result{}, nil } diff --git a/tink/sources/tink_events.go b/tink/sources/tink_events.go new file mode 100644 index 00000000..f23955ce --- /dev/null +++ b/tink/sources/tink_events.go @@ -0,0 +1,198 @@ +/* +Copyright 2020 The Kubernetes 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 sources contains custom controller-runtime sources for Tinkerbell. +package sources + +import ( + "context" + "errors" + "fmt" + "io" + "time" + + "github.com/go-logr/logr" + tinkinformers "github.com/tinkerbell/tink/client/informers" + tinkevents "github.com/tinkerbell/tink/protos/events" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + + tinkv1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" +) + +// TinkEventWatcher is a source that watches for Tinkerbell events and generates a GenericEvent +// for the associated CRD resource. +type TinkEventWatcher struct { + client client.Client + Logger logr.Logger + EventCh chan<- event.GenericEvent + ResourceType tinkevents.ResourceType +} + +func (w *TinkEventWatcher) getHardwareForID(ctx context.Context, id string) (*tinkv1.Hardware, error) { + hwList := &tinkv1.HardwareList{} + if err := w.client.List(ctx, hwList); err != nil { + return nil, fmt.Errorf("failed to list hardware: %w", err) + } + + for i, h := range hwList.Items { + if h.TinkID() == id { + w.Logger.Info("generating GenericEvent", "hardware", h.GetName()) + + return &hwList.Items[i], nil + } + } + + return nil, nil +} + +func (w *TinkEventWatcher) getTemplateForID(ctx context.Context, id string) (*tinkv1.Template, error) { + templateList := &tinkv1.TemplateList{} + if err := w.client.List(ctx, templateList); err != nil { + return nil, fmt.Errorf("failed to list templates: %w", err) + } + + for i, t := range templateList.Items { + if t.TinkID() == id { + w.Logger.Info("generating GenericEvent", "template", t.GetName()) + + return &templateList.Items[i], nil + } + } + + return nil, nil +} + +func (w *TinkEventWatcher) getWorkflowForID(ctx context.Context, id string) (*tinkv1.Workflow, error) { + workflowList := &tinkv1.WorkflowList{} + if err := w.client.List(ctx, workflowList); err != nil { + return nil, fmt.Errorf("failed to list workflows: %w", err) + } + + for i, wf := range workflowList.Items { + if wf.TinkID() == id { + w.Logger.Info("generating GenericEvent", "workflow", wf.GetName()) + + return &workflowList.Items[i], nil + } + } + + return nil, nil +} + +func (w *TinkEventWatcher) generateEventForTinkID(ctx context.Context, id string) error { + switch w.ResourceType { + case tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE: + hw, err := w.getHardwareForID(ctx, id) + if err != nil { + return err + } + + if hw != nil { + w.EventCh <- event.GenericEvent{ + Meta: hw, + Object: hw, + } + } + case tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE: + template, err := w.getTemplateForID(ctx, id) + if err != nil { + return err + } + + if template != nil { + w.EventCh <- event.GenericEvent{ + Meta: template, + Object: template, + } + } + case tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW: + workflow, err := w.getWorkflowForID(ctx, id) + if err != nil { + return err + } + + if workflow != nil { + w.EventCh <- event.GenericEvent{ + Meta: workflow, + Object: workflow, + } + } + default: + return fmt.Errorf("unknown resource type: %s", w.ResourceType.String()) + } + + return nil +} + +// NeedLeaderElection satisfies the controller-runtime LeaderElectionRunnable interface. +func (w *TinkEventWatcher) NeedLeaderElection() bool { + return true +} + +// InjectClient satisfies the controller-runtime Client injection interface. +func (w *TinkEventWatcher) InjectClient(c client.Client) error { + w.client = c + + return nil +} + +// Start starts the TinkEventWatcher. +func (w *TinkEventWatcher) Start(stopCh <-chan struct{}) error { + // TODO: currently this only triggers events for + // changes to workflows themselves, but not for updates + // to workflow_events, workflow_state, or workflow_data for + // a given workflow, need to figure out a way to trigger + // events for those. + now := time.Now() + + req := &tinkevents.WatchRequest{ + EventTypes: []tinkevents.EventType{ + tinkevents.EventType_EVENT_TYPE_CREATED, + tinkevents.EventType_EVENT_TYPE_UPDATED, + tinkevents.EventType_EVENT_TYPE_DELETED, + }, + ResourceTypes: []tinkevents.ResourceType{w.ResourceType}, + WatchEventsFrom: timestamppb.New(now), + } + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-stopCh + cancel() + }() + + tinkInformer := tinkinformers.New() + + w.Logger.Info("Starting Tinkerbell Informer", "resourceType", w.ResourceType.String()) + + err := tinkInformer.Start(ctx, req, func(e *tinkevents.Event) error { + if err := w.generateEventForTinkID(ctx, e.GetResourceId()); err != nil { + return err + } + + return nil + }) + if err != nil && !errors.Is(err, io.EOF) && status.Code(err) != codes.Canceled { + return fmt.Errorf("unexpected error from Tinkerbell informer: %w", err) + } + + return nil +} diff --git a/tink/sources/tink_events_internal_test.go b/tink/sources/tink_events_internal_test.go new file mode 100644 index 00000000..fe83c221 --- /dev/null +++ b/tink/sources/tink_events_internal_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2020 The Kubernetes 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 sources contains custom controller-runtime sources for Tinkerbell. +package sources + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + tinkv1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + tinkevents "github.com/tinkerbell/tink/protos/events" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func TestTinkEventWatcher_generateEventForTinkID(t *testing.T) { //nolint:funlen + g := NewWithT(t) + scheme := runtime.NewScheme() + + g.Expect(tinkv1.AddToScheme(scheme)).To(Succeed()) + + tests := []struct { + name string + id string + resourceType tinkevents.ResourceType + objs []runtime.Object + eventExpected bool + want event.GenericEvent + wantErr bool + }{ + { + name: "hardware not found", + id: "doesNotExist", + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE, + eventExpected: false, + wantErr: false, + }, + { + name: "hardware found", + id: "foo", + objs: []runtime.Object{ + &tinkv1.Hardware{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: tinkv1.HardwareSpec{ + ID: "foo", + }, + }, + }, + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_HARDWARE, + eventExpected: true, + want: event.GenericEvent{ + Meta: &tinkv1.Hardware{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: tinkv1.HardwareSpec{ + ID: "foo", + }, + }, + Object: &tinkv1.Hardware{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: tinkv1.HardwareSpec{ + ID: "foo", + }, + }, + }, + wantErr: false, + }, + { + name: "template not found", + id: "doesNotExist", + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE, + eventExpected: false, + wantErr: false, + }, + { + name: "template found", + id: "foo", + objs: []runtime.Object{ + &tinkv1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1.TemplateIDAnnotation: "foo", + }, + }, + }, + }, + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_TEMPLATE, + eventExpected: true, + want: event.GenericEvent{ + Meta: &tinkv1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1.TemplateIDAnnotation: "foo", + }, + }, + }, + Object: &tinkv1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1.TemplateIDAnnotation: "foo", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "workflow not found", + id: "doesNotExist", + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW, + eventExpected: false, + wantErr: false, + }, + { + name: "workflow found", + id: "foo", + objs: []runtime.Object{ + &tinkv1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1.WorkflowIDAnnotation: "foo", + }, + }, + }, + }, + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_WORKFLOW, + eventExpected: true, + want: event.GenericEvent{ + Meta: &tinkv1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1.WorkflowIDAnnotation: "foo", + }, + }, + }, + Object: &tinkv1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Annotations: map[string]string{ + tinkv1.WorkflowIDAnnotation: "foo", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "unknown resource type", + id: "doesNotExist", + resourceType: tinkevents.ResourceType_RESOURCE_TYPE_UNKNOWN, + wantErr: true, + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + eventCh := make(chan event.GenericEvent) + + w := &TinkEventWatcher{ + client: fake.NewFakeClientWithScheme(scheme, tt.objs...), + EventCh: eventCh, + Logger: log.Log, + ResourceType: tt.resourceType, + } + + go func() { + err := w.generateEventForTinkID(context.Background(), tt.id) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + + return + } + g.Expect(err).NotTo(HaveOccurred()) + }() + + if tt.eventExpected { + g.Eventually(eventCh).Should(Receive(BeEquivalentTo(tt.want))) + } else { + g.Consistently(eventCh).ShouldNot(Receive()) + } + }) + } +} diff --git a/tink/sources_test/tink_events_test.go b/tink/sources_test/tink_events_test.go new file mode 100644 index 00000000..7c43b8ff --- /dev/null +++ b/tink/sources_test/tink_events_test.go @@ -0,0 +1,374 @@ +/* +Copyright 2020 The Kubernetes 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 sources_test + +import ( + "context" + "crypto/x509" + "io" + "io/ioutil" + "net/http" + "os" + "testing" + + . "github.com/onsi/gomega" + tinkv1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" + "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/sources" + testutils "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/test/utils" + rawclient "github.com/tinkerbell/tink/client" + "github.com/tinkerbell/tink/protos/events" + "github.com/tinkerbell/tink/protos/hardware" + "github.com/tinkerbell/tink/protos/template" + "github.com/tinkerbell/tink/protos/workflow" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/klogr" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + sampleTemplate = ` +version: '0.1' +name: sample_workflow +global_timeout: 600 +tasks: + - name: "hello world first" + worker: "{{.device_1}}" + actions: + - name: "hello_world_first" + image: hello-world + timeout: 60 + - name: "hello world second" + worker: "{{.device_1}}" + actions: + - name: "hello_world_second" + image: hello-world + timeout: 60` +) + +func TestHardwareEvents(t *testing.T) { //nolint: funlen + g := NewWithT(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + scheme := runtime.NewScheme() + g.Expect(tinkv1.AddToScheme(scheme)).To(Succeed()) + + fakeClient := fake.NewFakeClientWithScheme(scheme) + conn := realConn(t) + hardwareClient := client.NewHardwareClient(hardware.NewHardwareServiceClient(conn)) + rawclient.EventsClient = events.NewEventsServiceClient(conn) + eventCh := make(chan event.GenericEvent) + + ctrl.SetLogger(klogr.New()) + + eventWatcher := sources.TinkEventWatcher{ + Logger: log.Log, + EventCh: eventCh, + ResourceType: events.ResourceType_RESOURCE_TYPE_HARDWARE, + } + + g.Expect(eventWatcher.InjectClient(fakeClient)).To(Succeed()) + + go func() { + g.Expect(eventWatcher.Start(ctx.Done())).Should(SatisfyAny(Succeed(), MatchError(io.EOF))) + }() + + // Create a Hardware resource in Tinkerbell + testHardware, err := testutils.GenerateHardware(2) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(hardwareClient.Create(ctx, testHardware)).To(Succeed()) + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we clean up the hardware resource we just created + g.Expect(hardwareClient.Delete(ctx, testHardware.Id)) + }() + + // Since we don't have a matching hardware resource in k8s + // we shouldn't see an event + g.Consistently(eventCh).ShouldNot(Receive()) + + // Create the hardware resource in k8s + hw := &tinkv1.Hardware{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: tinkv1.HardwareSpec{ + ID: testHardware.Id, + }, + } + g.Expect(fakeClient.Create(ctx, hw)).To(Succeed()) + + // Update the hardware + additionalInterface, err := testutils.GenerateHardwareInterface("") + g.Expect(err).NotTo(HaveOccurred()) + + testHardware.Network.Interfaces = append(testHardware.Network.Interfaces, additionalInterface) + g.Expect(hardwareClient.Update(ctx, testHardware)).To(Succeed()) + + expectedEvent := event.GenericEvent{ + Meta: hw, + Object: hw, + } + g.Eventually(eventCh).Should(Receive(BeEquivalentTo(expectedEvent))) +} + +func TestTemplateEvents(t *testing.T) { //nolint: funlen + g := NewWithT(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + scheme := runtime.NewScheme() + g.Expect(tinkv1.AddToScheme(scheme)).To(Succeed()) + + fakeClient := fake.NewFakeClientWithScheme(scheme) + conn := realConn(t) + templateClient := client.NewTemplateClient(template.NewTemplateServiceClient(conn)) + rawclient.EventsClient = events.NewEventsServiceClient(conn) + eventCh := make(chan event.GenericEvent) + + ctrl.SetLogger(klogr.New()) + + eventWatcher := sources.TinkEventWatcher{ + Logger: log.Log, + EventCh: eventCh, + ResourceType: events.ResourceType_RESOURCE_TYPE_TEMPLATE, + } + + g.Expect(eventWatcher.InjectClient(fakeClient)).To(Succeed()) + + go func() { + g.Expect(eventWatcher.Start(ctx.Done())).Should(SatisfyAny(Succeed(), MatchError(io.EOF))) + }() + + // Create the Template resource in Tinkerbell + testTemplate := testutils.GenerateTemplate("testTemplate", testutils.HelloWorldTemplate) + g.Expect(templateClient.Create(ctx, testTemplate)).To(Succeed()) + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we clean up the hardware resource we just created + g.Expect(templateClient.Delete(ctx, testTemplate.Id)) + }() + + // Since we don't have a matching template resource in k8s + // we shouldn't see an event + g.Consistently(eventCh).ShouldNot(Receive()) + + // Create the matching k8s resource + te := &tinkv1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: testTemplate.Name, + Annotations: map[string]string{ + tinkv1.TemplateIDAnnotation: testTemplate.Id, + }, + }, + Spec: tinkv1.TemplateSpec{ + Data: pointer.StringPtr(testTemplate.Data), + }, + } + g.Expect(fakeClient.Create(ctx, te)).To(Succeed()) + + // Update the template + testTemplate.Data = sampleTemplate + g.Expect(templateClient.Update(ctx, testTemplate)).To(Succeed()) + + expectedEvent := event.GenericEvent{ + Meta: te, + Object: te, + } + g.Eventually(eventCh).Should(Receive(BeEquivalentTo(expectedEvent))) +} + +func TestWorkflowEvents(t *testing.T) { //nolint: funlen + g := NewWithT(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + scheme := runtime.NewScheme() + g.Expect(tinkv1.AddToScheme(scheme)).To(Succeed()) + + fakeClient := fake.NewFakeClientWithScheme(scheme) + conn := realConn(t) + hardwareClient := client.NewHardwareClient(hardware.NewHardwareServiceClient(conn)) + templateClient := client.NewTemplateClient(template.NewTemplateServiceClient(conn)) + rawWorkflowClient := workflow.NewWorkflowServiceClient(conn) + workflowClient := client.NewWorkflowClient(rawWorkflowClient, hardwareClient) + rawclient.EventsClient = events.NewEventsServiceClient(conn) + eventCh := make(chan event.GenericEvent) + + ctrl.SetLogger(klogr.New()) + + eventWatcher := sources.TinkEventWatcher{ + Logger: log.Log, + EventCh: eventCh, + ResourceType: events.ResourceType_RESOURCE_TYPE_WORKFLOW, + } + + g.Expect(eventWatcher.InjectClient(fakeClient)).To(Succeed()) + + go func() { + g.Expect(eventWatcher.Start(ctx.Done())).Should(SatisfyAny(Succeed(), MatchError(io.EOF))) + }() + + // Create a Hardware resource in Tinkerbell + testHardware, err := testutils.GenerateHardware(2) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(hardwareClient.Create(ctx, testHardware)).To(Succeed()) + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we clean up the hardware resource we just created + g.Expect(hardwareClient.Delete(ctx, testHardware.Id)) + }() + + // Create the Template resource in Tinkerbell + testTemplate := testutils.GenerateTemplate("testTemplate", testutils.HelloWorldTemplate) + g.Expect(templateClient.Create(ctx, testTemplate)).To(Succeed()) + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we clean up the template resource we just created + g.Expect(templateClient.Delete(ctx, testTemplate.Id)) + }() + + // Create the Workflow resource in Tinkerbell + workflowID, err := workflowClient.Create(ctx, testTemplate.Id, testHardware.Id) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(workflowID).NotTo(BeEmpty()) + + // Attempt to cleanup even if later assertions fail + defer func() { + // Ensure that we clean up the workflow resource we just created + g.Expect(workflowClient.Delete(ctx, workflowID)) + }() + + // Since we don't have a matching template resource in k8s + // we shouldn't see an event + g.Consistently(eventCh).ShouldNot(Receive()) + + // Create the matching k8s resources + te := &tinkv1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: testTemplate.Name, + Annotations: map[string]string{ + tinkv1.TemplateIDAnnotation: testTemplate.Id, + }, + }, + Spec: tinkv1.TemplateSpec{ + Data: pointer.StringPtr(testTemplate.Data), + }, + } + + g.Expect(fakeClient.Create(ctx, te)).To(Succeed()) + + h := &tinkv1.Hardware{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testHardware", + }, + Spec: tinkv1.HardwareSpec{ + ID: testHardware.Id, + }, + } + + g.Expect(fakeClient.Create(ctx, h)).To(Succeed()) + + w := &tinkv1.Workflow{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testWorkflow", + Annotations: map[string]string{ + tinkv1.WorkflowIDAnnotation: workflowID, + }, + }, + Spec: tinkv1.WorkflowSpec{ + TemplateRef: te.Name, + HardwareRef: h.Name, + }, + } + g.Expect(fakeClient.Create(ctx, w)).To(Succeed()) + + // // check for event following updateworkflowdata + // update := &workflow.UpdateWorkflowDataRequest{ + // WorkflowId: workflowID, + // Metadata: []byte(`{"WorkerID": "3"}`), + // Data: []byte(`{"version": "0.1"}`), + // } + // _, err = rawWorkflowClient.UpdateWorkflowData(ctx, update) + // g.Expect(err).NotTo(HaveOccurred()) + + // expectedEvent := event.GenericEvent{ + // Meta: w, + // Object: w, + // } + // g.Eventually(eventCh).Should(Receive(BeEquivalentTo(expectedEvent))) + + // // check for event following reporting action status + // status := &workflow.WorkflowActionStatus{ + // WorkflowId: workflowID, + // TaskName: "hello world", + // ActionName: "hello_world", + // ActionStatus: workflow.State_STATE_PENDING, + // WorkerId: uuid.New().String(), + // } + // _, err = rawWorkflowClient.ReportActionStatus(ctx, status) + // g.Expect(err).NotTo(HaveOccurred()) + + // g.Eventually(eventCh).Should(Receive(BeEquivalentTo(expectedEvent))) +} + +func realConn(t *testing.T) *grpc.ClientConn { + g := NewWithT(t) + + certURL, ok := os.LookupEnv("TINKERBELL_CERT_URL") + if !ok || certURL == "" { + t.Skip("Skipping live client tests because TINKERBELL_CERT_URL is not set.") + } + + grpcAuthority, ok := os.LookupEnv("TINKERBELL_GRPC_AUTHORITY") + if !ok || grpcAuthority == "" { + t.Skip("Skipping live client tests because TINKERBELL_GRPC_AUTHORITY is not set.") + } + + resp, err := http.Get(certURL) //nolint:noctx + g.Expect(err).NotTo(HaveOccurred()) + + defer resp.Body.Close() //nolint:errcheck + + certs, err := ioutil.ReadAll(resp.Body) + g.Expect(err).NotTo(HaveOccurred()) + + cp := x509.NewCertPool() + ok = cp.AppendCertsFromPEM(certs) + g.Expect(ok).To(BeTrue()) + + creds := credentials.NewClientTLSFromCert(cp, "tink-server") + conn, err := grpc.Dial(grpcAuthority, grpc.WithTransportCredentials(creds)) + g.Expect(err).NotTo(HaveOccurred()) + + return conn +} diff --git a/tink/test/utils/generators.go b/tink/test/utils/generators.go new file mode 100644 index 00000000..7fa90e48 --- /dev/null +++ b/tink/test/utils/generators.go @@ -0,0 +1,189 @@ +/* +Copyright 2020 The Kubernetes 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 utils + +import ( + "crypto/rand" + "fmt" + "math/big" + "net" + "sync" + + "github.com/tinkerbell/tink/protos/hardware" + "github.com/tinkerbell/tink/protos/template" +) + +const HelloWorldTemplate = `version: "0.1" +name: hello_world_workflow +global_timeout: 600 +tasks: + - name: "hello world" + worker: "{{.device_1}}" + actions: + - name: "hello_world" + image: hello-world + timeout: 60` + +// These are CIDRs that we should not come across in a real +// environment, since they are reserved for use in documentation +// and examples. +var testCIDRs = [...]string{ + "192.0.2.0/24", + "198.51.100.0/24", + "203.0.113.0/24", +} + +var IPGetter = ipGetter{ + addresses: make(map[string]string), +} + +type ipGetter struct { + addresses map[string]string + lock sync.Mutex +} + +func (i *ipGetter) nextAddressFromCIDR(cidr string) (string, string, string, error) { + i.lock.Lock() + defer i.lock.Unlock() + + _, network, err := net.ParseCIDR(cidr) + if err != nil { + return "", "", "", fmt.Errorf("failed to parse cidr: %w", err) + } + + netMask := net.IP(network.Mask).String() + + // Use the first available address as the gateway address + gw := make(net.IP, len(network.IP)) + copy(gw, network.IP) + gw[len(gw)-1]++ + gateway := gw.String() + + // Attempt to get the last address used, othewise use the + // gateway address as the starting point + lastAddress, ok := i.addresses[cidr] + if !ok { + lastAddress = gateway + } + + // Get the next IP by incrementing lastAddress + nextIP := net.ParseIP(lastAddress) + nextIP[len(nextIP)-1]++ + + ip := nextIP.String() + + // Store the last address + i.addresses[cidr] = ip + + return ip, netMask, gateway, nil +} + +var MACGenerator = macGenerator{ + addresses: make(map[string]struct{}), +} + +type macGenerator struct { + addresses map[string]struct{} + lock sync.Mutex +} + +func (m *macGenerator) Get() (string, error) { + m.lock.Lock() + defer m.lock.Unlock() + + for { + mac := net.HardwareAddr(make([]byte, 6)) + + _, err := rand.Read(mac) + if err != nil { + return "", fmt.Errorf("failed to generate random mac: %w", err) + } + + // Ensure the individual bit is set + mac[0] &= ^byte(1) + + // Ensure the local bit is set + mac[0] |= byte(2) + + key := mac.String() + if _, found := m.addresses[key]; !found { + m.addresses[key] = struct{}{} + + return key, nil + } + } +} + +func GenerateTemplate(name, data string) *template.WorkflowTemplate { + return &template.WorkflowTemplate{ + Name: name, + Data: data, + } +} + +func GenerateHardware(numInterfaces int) (*hardware.Hardware, error) { + hw := &hardware.Hardware{ + Network: &hardware.Hardware_Network{}, + } + + for i := 0; i < numInterfaces; i++ { + cidr := testCIDRs[i%len(testCIDRs)] + + ni, err := GenerateHardwareInterface(cidr) + if err != nil { + return nil, err + } + + hw.Network.Interfaces = append(hw.Network.Interfaces, ni) + } + + return hw, nil +} + +func GenerateHardwareInterface(cidr string) (*hardware.Hardware_Network_Interface, error) { + if cidr == "" { + i, err := rand.Int(rand.Reader, big.NewInt(int64(len(testCIDRs)))) + if err != nil { + return nil, fmt.Errorf("failed to get random index for cidr: %w", err) + } + + cidr = testCIDRs[i.Int64()] + } + + ip, netmask, gateway, err := IPGetter.nextAddressFromCIDR(cidr) + if err != nil { + return nil, err + } + + mac, err := MACGenerator.Get() + if err != nil { + return nil, err + } + + ni := &hardware.Hardware_Network_Interface{ + Dhcp: &hardware.Hardware_DHCP{ + Mac: mac, + Ip: &hardware.Hardware_DHCP_IP{ + Address: ip, + Netmask: netmask, + Gateway: gateway, + }, + }, + } + + return ni, nil +} From a40f06d0cdb5a34b4859a929c6d7bba235d83651 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 23 Dec 2020 10:45:13 -0500 Subject: [PATCH 12/15] fixes --- .../crd/bases/tinkerbell.org_templates.yaml | 3 ++ tink/client/hardware.go | 2 ++ tink/controllers/hardware/controller.go | 4 +-- tink/controllers/template/controller.go | 2 +- tink/controllers/workflow/controller.go | 31 +++++-------------- 5 files changed, 15 insertions(+), 27 deletions(-) diff --git a/config/crd/bases/tinkerbell.org_templates.yaml b/config/crd/bases/tinkerbell.org_templates.yaml index 7017ea97..73e40ba3 100644 --- a/config/crd/bases/tinkerbell.org_templates.yaml +++ b/config/crd/bases/tinkerbell.org_templates.yaml @@ -43,6 +43,9 @@ spec: type: object status: description: TemplateStatus defines the observed state of Template. + properties: + state: + type: string type: object type: object served: true diff --git a/tink/client/hardware.go b/tink/client/hardware.go index c7057faf..783ee472 100644 --- a/tink/client/hardware.go +++ b/tink/client/hardware.go @@ -78,6 +78,8 @@ func (t *Hardware) Get(ctx context.Context, id, ip, mac string) (*hardware.Hardw case ip != "": req.Ip = ip method = t.client.ByIP + default: + return nil, errors.New("need to specify either id, ip, or mac") } tinkHardware, err := method(ctx, req) diff --git a/tink/controllers/hardware/controller.go b/tink/controllers/hardware/controller.go index 2d5bba38..7822af9e 100644 --- a/tink/controllers/hardware/controller.go +++ b/tink/controllers/hardware/controller.go @@ -99,7 +99,7 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, h *tinkv1alpha1.Hardwa h.Status.State = tinkv1alpha1.HardwareError - if err := r.Client.Patch(ctx, h, patch); err != nil { + if err := r.Client.Status().Patch(ctx, h, patch); err != nil { logger.Error(err, "Failed to patch hardware") return ctrl.Result{}, fmt.Errorf("failed to patch hardware: %w", err) @@ -176,7 +176,7 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, h *tinkv1alpha1.Hardwa h.Status.State = v1alpha1.HardwareReady - if err := r.Client.Patch(ctx, h, patch); err != nil { + if err := r.Client.Status().Patch(ctx, h, patch); err != nil { logger.Error(err, "Failed to patch hardware") return ctrl.Result{}, fmt.Errorf("failed to patch hardware: %w", err) diff --git a/tink/controllers/template/controller.go b/tink/controllers/template/controller.go index 22856de1..dc8999cc 100644 --- a/tink/controllers/template/controller.go +++ b/tink/controllers/template/controller.go @@ -154,7 +154,7 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, t *tinkv1alpha1.Templa t.Status.State = v1alpha1.TemplateReady - if err := r.Client.Patch(ctx, t, patch); err != nil { + if err := r.Client.Status().Patch(ctx, t, patch); err != nil { logger.Error(err, "Failed to patch template") return ctrl.Result{}, fmt.Errorf("failed to patch template: %w", err) diff --git a/tink/controllers/workflow/controller.go b/tink/controllers/workflow/controller.go index 1b50b5e1..6bd42962 100644 --- a/tink/controllers/workflow/controller.go +++ b/tink/controllers/workflow/controller.go @@ -97,28 +97,20 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { func (r *Reconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { logger := r.Log.WithValues("workflow", w.Name) - var workflowID string + workflowID := w.TinkID() - tinkWorkflow, err := r.getWorkflow(ctx, w) - - switch { - case errors.Is(err, tinkclient.ErrNotFound): + if workflowID == "" { id, err := r.createWorkflow(ctx, w) if err != nil { return ctrl.Result{}, err } workflowID = id + } - tinkWorkflow, err = r.getWorkflow(ctx, w) - if err != nil { - return ctrl.Result{}, err - } - case err != nil: + tinkWorkflow, err := r.WorkflowClient.Get(ctx, workflowID) + if err != nil { return ctrl.Result{}, err - - default: - workflowID = tinkWorkflow.Id } if err := common.EnsureAnnotation(ctx, r.Client, logger, w, tinkv1alpha1.WorkflowIDAnnotation, @@ -136,7 +128,7 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, w *tinkv1alpha1.Workfl w.Status.Data = tinkWorkflow.GetData() w.Status.State = tinkWorkflow.GetState().String() - if err := r.Client.Patch(ctx, w, patch); err != nil { + if err := r.Client.Status().Patch(ctx, w, patch); err != nil { logger.Error(err, "Failed to patch workflow") return ctrl.Result{}, fmt.Errorf("failed to patch workflow: %w", err) @@ -167,7 +159,7 @@ func (r *Reconciler) createWorkflow(ctx context.Context, w *tinkv1alpha1.Workflo } id, err := r.WorkflowClient.Create( - ctx, t.GetObjectMeta().GetAnnotations()[tinkv1alpha1.TemplateIDAnnotation], + ctx, t.TinkID(), hw.Spec.ID, ) if err != nil { @@ -179,15 +171,6 @@ func (r *Reconciler) createWorkflow(ctx context.Context, w *tinkv1alpha1.Workflo return id, nil } -func (r *Reconciler) getWorkflow(ctx context.Context, w *tinkv1alpha1.Workflow) (*workflow.Workflow, error) { - annotations := w.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - - return r.WorkflowClient.Get(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation]) -} - func (r *Reconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workflow) (ctrl.Result, error) { // Create a patch for use later patch := client.MergeFrom(w.DeepCopy()) From 9126feb01071c2ce62a2e4463ff975435eb81aa5 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 23 Dec 2020 16:05:19 -0500 Subject: [PATCH 13/15] fixes/cleanup --- .../crd/bases/tinkerbell.org_workflows.yaml | 66 ++++++++++- tink/api/v1alpha1/template_types.go | 9 ++ tink/api/v1alpha1/workflow_types.go | 44 ++++++- tink/api/v1alpha1/zz_generated.deepcopy.go | 72 +++++++++++- tink/client/workflow.go | 107 ++++++++++++++++++ tink/controllers/common/common.go | 24 ---- tink/controllers/common_test/common_test.go | 64 ----------- tink/controllers/template/controller.go | 51 ++++----- .../template/controller_internal_test.go | 7 +- tink/controllers/workflow/controller.go | 98 +++++++++++++--- 10 files changed, 401 insertions(+), 141 deletions(-) diff --git a/config/crd/bases/tinkerbell.org_workflows.yaml b/config/crd/bases/tinkerbell.org_workflows.yaml index 606a5ae5..be71195e 100644 --- a/config/crd/bases/tinkerbell.org_workflows.yaml +++ b/config/crd/bases/tinkerbell.org_workflows.yaml @@ -41,10 +41,6 @@ spec: hardwareRef: description: Name of the Hardware associated with this workflow. type: string - hardwareTemplate: - description: HardwareTemplate is the template used for creating the - workflow. - type: string templateRef: description: Name of the Template associated with this workflow. type: string @@ -52,9 +48,71 @@ spec: status: description: WorkflowStatus defines the observed state of Workflow. properties: + actions: + description: Actions are the actions for this Workflow. + items: + properties: + command: + items: + type: string + type: array + environment: + items: + type: string + type: array + image: + type: string + name: + type: string + on_failure: + items: + type: string + type: array + on_timeout: + items: + type: string + type: array + task_name: + type: string + timeout: + format: int64 + type: integer + volumes: + items: + type: string + type: array + worker_id: + type: string + type: object + type: array data: description: Data is the populated Workflow Data in Tinkerbell. type: string + events: + description: Events are events for this Workflow. + items: + properties: + action_name: + type: string + action_status: + type: string + created_at: + format: date-time + type: string + message: + type: string + seconds: + format: int64 + type: integer + task_name: + type: string + worker_id: + type: string + type: object + type: array + metadata: + description: Metadata is the metadata stored in Tinkerbell. + type: string state: description: State is the state of the workflow in Tinkerbell. type: string diff --git a/tink/api/v1alpha1/template_types.go b/tink/api/v1alpha1/template_types.go index 819f1038..36f3f257 100644 --- a/tink/api/v1alpha1/template_types.go +++ b/tink/api/v1alpha1/template_types.go @@ -68,6 +68,15 @@ func (t *Template) TinkID() string { return annotations[TemplateIDAnnotation] } +// SetTinkID sets the Tinkerbell ID associated with this Template. +func (t *Template) SetTinkID(id string) { + if t.GetAnnotations() == nil { + t.SetAnnotations(make(map[string]string)) + } + + t.Annotations[TemplateIDAnnotation] = id +} + // +kubebuilder:object:root=true // TemplateList contains a list of Templates. diff --git a/tink/api/v1alpha1/workflow_types.go b/tink/api/v1alpha1/workflow_types.go index 66249aea..160068d0 100644 --- a/tink/api/v1alpha1/workflow_types.go +++ b/tink/api/v1alpha1/workflow_types.go @@ -35,9 +35,6 @@ type WorkflowSpec struct { // Name of the Hardware associated with this workflow. HardwareRef string `json:"hardwareRef,omitempty"` - - // HardwareTemplate is the template used for creating the workflow. - HardwareTemplate string `json:"hardwareTemplate,omitempty"` } // WorkflowStatus defines the observed state of Workflow. @@ -47,6 +44,38 @@ type WorkflowStatus struct { // Data is the populated Workflow Data in Tinkerbell. Data string `json:"data,omitempty"` + + // Metadata is the metadata stored in Tinkerbell. + Metadata string `json:"metadata,omitempty"` + + // Actions are the actions for this Workflow. + Actions []Action `json:"actions,omitempty"` + + // Events are events for this Workflow. + Events []Event `json:"events,omitempty"` +} + +type Action struct { + TaskName string `json:"task_name,omitempty"` + Name string `json:"name,omitempty"` + Image string `json:"image,omitempty"` + Timeout int64 `json:"timeout,omitempty"` + Command []string `json:"command,omitempty"` + OnTimeout []string `json:"on_timeout,omitempty"` + OnFailure []string `json:"on_failure,omitempty"` + WorkerID string `json:"worker_id,omitempty"` + Volumes []string `json:"volumes,omitempty"` + Environment []string `json:"environment,omitempty"` +} + +type Event struct { + TaskName string `json:"task_name,omitempty"` + ActionName string `json:"action_name,omitempty"` + ActionStatus string `json:"action_status,omitempty"` + Seconds int64 `json:"seconds,omitempty"` + Message string `json:"message,omitempty"` + CreatedAt metav1.Time `json:"created_at,omitempty"` + WorkerID string `json:"worker_id,omitempty"` } // +kubebuilder:subresource:status @@ -73,6 +102,15 @@ func (w *Workflow) TinkID() string { return annotations[WorkflowIDAnnotation] } +// SetTinkID sets the Tinkerbell ID associated with this Workflow. +func (w *Workflow) SetTinkID(id string) { + if w.GetAnnotations() == nil { + w.SetAnnotations(make(map[string]string)) + } + + w.Annotations[WorkflowIDAnnotation] = id +} + // +kubebuilder:object:root=true // WorkflowList contains a list of Workflows. diff --git a/tink/api/v1alpha1/zz_generated.deepcopy.go b/tink/api/v1alpha1/zz_generated.deepcopy.go index afedebfe..a7bda64b 100644 --- a/tink/api/v1alpha1/zz_generated.deepcopy.go +++ b/tink/api/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,46 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Action) DeepCopyInto(out *Action) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.OnTimeout != nil { + in, out := &in.OnTimeout, &out.OnTimeout + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.OnFailure != nil { + in, out := &in.OnFailure, &out.OnFailure + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Environment != nil { + in, out := &in.Environment, &out.Environment + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Action. +func (in *Action) DeepCopy() *Action { + if in == nil { + return nil + } + out := new(Action) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DHCP) DeepCopyInto(out *DHCP) { *out = *in @@ -54,6 +94,22 @@ func (in *DHCP) DeepCopy() *DHCP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Event) DeepCopyInto(out *Event) { + *out = *in + in.CreatedAt.DeepCopyInto(&out.CreatedAt) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Event. +func (in *Event) DeepCopy() *Event { + if in == nil { + return nil + } + out := new(Event) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Hardware) DeepCopyInto(out *Hardware) { *out = *in @@ -355,7 +411,7 @@ func (in *Workflow) DeepCopyInto(out *Workflow) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Workflow. @@ -426,6 +482,20 @@ func (in *WorkflowSpec) DeepCopy() *WorkflowSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkflowStatus) DeepCopyInto(out *WorkflowStatus) { *out = *in + if in.Actions != nil { + in, out := &in.Actions, &out.Actions + *out = make([]Action, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Events != nil { + in, out := &in.Events, &out.Events + *out = make([]Event, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStatus. diff --git a/tink/client/workflow.go b/tink/client/workflow.go index 347d18a0..9a305e61 100644 --- a/tink/client/workflow.go +++ b/tink/client/workflow.go @@ -19,7 +19,9 @@ package client import ( "context" "encoding/json" + "errors" "fmt" + "io" "github.com/tinkerbell/tink/protos/hardware" "github.com/tinkerbell/tink/protos/workflow" @@ -50,6 +52,111 @@ func (t *Workflow) Get(ctx context.Context, id string) (*workflow.Workflow, erro return tinkWorkflow, nil } +func (t *Workflow) GetMetadata(ctx context.Context, id string) ([]byte, error) { + verReq := &workflow.GetWorkflowDataRequest{WorkflowId: id} + + verResp, err := t.client.GetWorkflowDataVersion(ctx, verReq) + if err != nil { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { + return nil, fmt.Errorf("workflow %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get workflow version from Tinkerbell: %w", err) + } + + req := &workflow.GetWorkflowDataRequest{WorkflowId: id, Version: verResp.GetVersion()} + + resp, err := t.client.GetWorkflowMetadata(ctx, req) + if err != nil { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { + return nil, fmt.Errorf("workflow %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get workflow metadata from Tinkerbell: %w", err) + } + + return resp.GetData(), nil +} + +func (t *Workflow) GetActions(ctx context.Context, id string) ([]*workflow.WorkflowAction, error) { + req := &workflow.WorkflowActionsRequest{WorkflowId: id} + + resp, err := t.client.GetWorkflowActions(ctx, req) + if err != nil { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { + return nil, fmt.Errorf("workflow %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get workflow actions from Tinkerbell: %w", err) + } + + return resp.GetActionList(), nil +} + +func (t *Workflow) GetEvents(ctx context.Context, id string) ([]*workflow.WorkflowActionStatus, error) { + req := &workflow.GetRequest{Id: id} + + resp, err := t.client.ShowWorkflowEvents(ctx, req) + if err != nil { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { + return nil, fmt.Errorf("workflow %w", ErrNotFound) + } + + return nil, fmt.Errorf("failed to get workflow events from Tinkerbell: %w", err) + } + + result := []*workflow.WorkflowActionStatus{} + + for { + e, err := resp.Recv() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, fmt.Errorf("failed to get workflow event from Tinkerbell: %w", err) + } + + result = append(result, e) + } + + return result, nil +} + +func (t *Workflow) GetState(ctx context.Context, id string) (workflow.State, error) { + req := &workflow.GetRequest{Id: id} + + resp, err := t.client.GetWorkflowContext(ctx, req) + if err != nil { + if err.Error() == sqlErrorString || err.Error() == sqlErrorStringAlt { + return 0, fmt.Errorf("workflow %w", ErrNotFound) + } + + return 0, fmt.Errorf("failed to get workflow state from Tinkerbell: %w", err) + } + + currIndex := resp.GetCurrentActionIndex() + total := resp.GetTotalNumberOfActions() + currState := resp.GetCurrentActionState() + + switch { + case total == 0: + // If there are no actions, let's just call it pending + return workflow.State_STATE_PENDING, nil + case currIndex+1 == total: + // If we are on the last action, just report it's state + return currState, nil + case currState != workflow.State_STATE_SUCCESS: + // If the state of the last action is anything other than + // success, just report it's state. + return currState, nil + default: + // We are not on the last action, and the last action + // was successful, we should report pending + return workflow.State_STATE_PENDING, nil + } +} + // Create a Tinkerbell Workflow. func (t *Workflow) Create(ctx context.Context, templateID, hardwareID string) (string, error) { h, err := t.hardwareClient.Get(ctx, hardwareID, "", "") diff --git a/tink/controllers/common/common.go b/tink/controllers/common/common.go index f1e9ef42..fa990bc2 100644 --- a/tink/controllers/common/common.go +++ b/tink/controllers/common/common.go @@ -53,27 +53,3 @@ func EnsureFinalizer(ctx context.Context, c client.Client, logger logr.Logger, o return nil } - -// EnsureAnnotation ensures the given annotation key and value are applied -// to the resource. -func EnsureAnnotation(ctx context.Context, c client.Client, logger logr.Logger, obj Object, key, value string) error { - patch := client.MergeFrom(obj.DeepCopyObject()) - - annotations := obj.GetAnnotations() - - if annotations == nil { - annotations = map[string]string{} - } - - annotations[key] = value - - obj.SetAnnotations(annotations) - - if err := c.Patch(ctx, obj, patch); err != nil { - logger.Error(err, "Failed to add annotation to resource") - - return fmt.Errorf("failed to add annotation to resource: %w", err) - } - - return nil -} diff --git a/tink/controllers/common_test/common_test.go b/tink/controllers/common_test/common_test.go index 961984e9..ccc8764c 100644 --- a/tink/controllers/common_test/common_test.go +++ b/tink/controllers/common_test/common_test.go @@ -88,67 +88,3 @@ func Test_EnsureFinalizer(t *testing.T) { }) } } - -func Test_EnsureAnnotation(t *testing.T) { - g := NewWithT(t) - scheme := runtime.NewScheme() - - g.Expect(tinkv1alpha1.AddToScheme(scheme)).To(Succeed()) - - tests := []struct { - name string - in common.Object - annotationKey string - annotationValue string - wantErr bool - }{ - { - name: "Adds annotation when not present", - in: &tinkv1alpha1.Template{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - Spec: tinkv1alpha1.TemplateSpec{}, - }, - annotationKey: "key", - annotationValue: "value", - wantErr: false, - }, - { - name: "Annotation already exists", - in: &tinkv1alpha1.Template{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Annotations: map[string]string{ - "color": "green", - }, - }, - Spec: tinkv1alpha1.TemplateSpec{}, - }, - annotationKey: "color", - annotationValue: "blue", - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - ctx := context.Background() - fakeClient := fake.NewFakeClientWithScheme(scheme, tt.in.DeepCopyObject()) - - err := common.EnsureAnnotation(ctx, fakeClient, log.Log, tt.in, tt.annotationKey, tt.annotationValue) - if tt.wantErr { - g.Expect(err).To(HaveOccurred()) - - return - } - g.Expect(err).NotTo(HaveOccurred()) - - actual := &tinkv1alpha1.Template{} - key := client.ObjectKey{Name: tt.in.GetName()} - g.Expect(fakeClient.Get(ctx, key, actual)).To(Succeed()) - g.Expect(actual.Annotations).To(HaveKeyWithValue(tt.annotationKey, tt.annotationValue)) - }) - } -} diff --git a/tink/controllers/template/controller.go b/tink/controllers/template/controller.go index dc8999cc..c20aade2 100644 --- a/tink/controllers/template/controller.go +++ b/tink/controllers/template/controller.go @@ -23,7 +23,6 @@ import ( "fmt" "github.com/go-logr/logr" - "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" tinkclient "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/client" "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/common" @@ -99,11 +98,19 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { func (r *Reconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { // Create a patch for use later - patch := client.MergeFrom(t.DeepCopy()) - logger := r.Log.WithValues("template", t.Name) - tinkTemplate, err := r.getTemplate(ctx, t) + templateID := t.TinkID() + if templateID == "" { + tinkTemplate, err := r.createTemplate(ctx, t) + if err != nil { + return ctrl.Result{}, err + } + + templateID = tinkTemplate.Id + } + + tinkTemplate, err := r.TemplateClient.Get(ctx, templateID, t.Name) switch { case errors.Is(err, tinkclient.ErrNotFound): @@ -114,7 +121,17 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Templa tinkTemplate = result case err != nil: - return ctrl.Result{}, err + return ctrl.Result{}, fmt.Errorf("failed to get template: %w", err) + } + + // Make sure that we record the tinkerbell id for the workflow + patch := client.MergeFrom(t.DeepCopy()) + t.SetTinkID(templateID) + + if err := r.Client.Patch(ctx, t, patch); err != nil { + logger.Error(err, "Failed to patch template") + + return ctrl.Result{}, fmt.Errorf("failed to patch template: %w", err) } // If the data is specified and different than what is stored in Tinkerbell, @@ -128,11 +145,7 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, t *tinkv1alpha1.Templa } } - if err := common.EnsureAnnotation(ctx, r.Client, logger, t, tinkv1alpha1.TemplateIDAnnotation, - tinkTemplate.Id); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to ensure id annotation on template: %w", err) - } - + patch = client.MergeFrom(t.DeepCopy()) // If data was not specified, we are importing a pre-existing resource and should // populate it from Tinkerbell if t.Spec.Data == nil { @@ -152,7 +165,7 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, t *tinkv1alpha1.Templa logger := r.Log.WithValues("template", t.Name) patch := client.MergeFrom(t.DeepCopy()) - t.Status.State = v1alpha1.TemplateReady + t.Status.State = tinkv1alpha1.TemplateReady if err := r.Client.Status().Patch(ctx, t, patch); err != nil { logger.Error(err, "Failed to patch template") @@ -180,28 +193,14 @@ func (r *Reconciler) createTemplate(ctx context.Context, t *tinkv1alpha1.Templat return tinkTemplate, nil } -func (r *Reconciler) getTemplate(ctx context.Context, t *tinkv1alpha1.Template) (*template.WorkflowTemplate, error) { - annotations := t.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - - return r.TemplateClient.Get(ctx, annotations[tinkv1alpha1.TemplateIDAnnotation], t.Name) -} - func (r *Reconciler) reconcileDelete(ctx context.Context, t *tinkv1alpha1.Template) (ctrl.Result, error) { // Create a patch for use later patch := client.MergeFrom(t.DeepCopy()) logger := r.Log.WithValues("template", t.Name) - annotations := t.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - // If we've recorded an ID for the Template, then we should delete it - if id, ok := annotations[tinkv1alpha1.TemplateIDAnnotation]; ok { + if id := t.TinkID(); id != "" { err := r.TemplateClient.Delete(ctx, id) if err != nil && !errors.Is(err, tinkclient.ErrNotFound) { logger.Error(err, "Failed to delete template from Tinkerbell") diff --git a/tink/controllers/template/controller_internal_test.go b/tink/controllers/template/controller_internal_test.go index d37bc8bc..9d3aea04 100644 --- a/tink/controllers/template/controller_internal_test.go +++ b/tink/controllers/template/controller_internal_test.go @@ -227,10 +227,7 @@ func TestTemplateReconciler_reconcileNormal(t *testing.T) { key := client.ObjectKey{Name: tt.in.Name} g.Expect(r.Client.Get(context.Background(), key, k8sTemplate)).To(Succeed()) - // expect the k8s resource to have an id stored - g.Expect(k8sTemplate.GetAnnotations()).To(HaveKey(tinkv1alpha1.TemplateIDAnnotation)) - - id := k8sTemplate.Annotations[tinkv1alpha1.TemplateIDAnnotation] + id := k8sTemplate.TinkID() // Expect the id to be non-empty g.Expect(id).NotTo(BeEmpty()) @@ -242,7 +239,7 @@ func TestTemplateReconciler_reconcileNormal(t *testing.T) { tinkTemplate := fakeTemplateClient.Objs[id] // Verify the IDs match - g.Expect(tinkTemplate.Id, k8sTemplate.Annotations[tinkv1alpha1.TemplateIDAnnotation]) + g.Expect(tinkTemplate.Id, id) // Verify the Names match g.Expect(tinkTemplate.Name).To(BeEquivalentTo(k8sTemplate.Name)) diff --git a/tink/controllers/workflow/controller.go b/tink/controllers/workflow/controller.go index 6bd42962..7f6d3f7b 100644 --- a/tink/controllers/workflow/controller.go +++ b/tink/controllers/workflow/controller.go @@ -28,6 +28,7 @@ import ( "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/controllers/common" "github.com/tinkerbell/tink/protos/workflow" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -41,6 +42,10 @@ type workflowClient interface { Get(ctx context.Context, id string) (*workflow.Workflow, error) Create(ctx context.Context, templateID, hardwareID string) (string, error) Delete(ctx context.Context, id string) error + GetMetadata(ctx context.Context, id string) ([]byte, error) + GetActions(ctx context.Context, id string) ([]*workflow.WorkflowAction, error) + GetEvents(ctx context.Context, id string) ([]*workflow.WorkflowActionStatus, error) + GetState(ctx context.Context, id string) (workflow.State, error) } // Reconciler implements Reconciler interface by managing Tinkerbell workflows. @@ -110,12 +115,17 @@ func (r *Reconciler) reconcileNormal(ctx context.Context, w *tinkv1alpha1.Workfl tinkWorkflow, err := r.WorkflowClient.Get(ctx, workflowID) if err != nil { - return ctrl.Result{}, err + return ctrl.Result{}, fmt.Errorf("failed to get workflow: %w", err) } - if err := common.EnsureAnnotation(ctx, r.Client, logger, w, tinkv1alpha1.WorkflowIDAnnotation, - workflowID); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to ensure id annotation on workflow: %w", err) + // Make sure that we record the tinkerbell id for the workflow + patch := client.MergeFrom(w.DeepCopy()) + w.SetTinkID(workflowID) + + if err := r.Client.Patch(ctx, w, patch); err != nil { + logger.Error(err, "Failed to patch workflow") + + return ctrl.Result{}, fmt.Errorf("failed to patch workflow: %w", err) } return r.reconcileStatus(ctx, w, tinkWorkflow) @@ -125,15 +135,80 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, w *tinkv1alpha1.Workfl logger := r.Log.WithValues("workflow", w.Name) patch := client.MergeFrom(w.DeepCopy()) + // Try to patch what we have even if we hit errors gather additional status information + defer func() { + if err := r.Client.Status().Patch(ctx, w, patch); err != nil { + logger.Error(err, "Failed to patch workflow status") + } + }() + w.Status.Data = tinkWorkflow.GetData() - w.Status.State = tinkWorkflow.GetState().String() - if err := r.Client.Status().Patch(ctx, w, patch); err != nil { - logger.Error(err, "Failed to patch workflow") + md, err := r.WorkflowClient.GetMetadata(ctx, tinkWorkflow.GetId()) + if err != nil { + logger.Error(err, "Failed to get metadata for workflow") - return ctrl.Result{}, fmt.Errorf("failed to patch workflow: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to get metadata for workflow: %w", err) + } + + w.Status.Metadata = string(md) + + state, err := r.WorkflowClient.GetState(ctx, tinkWorkflow.GetId()) + if err != nil { + logger.Error(err, "Failed to get state for workflow") + + return ctrl.Result{}, fmt.Errorf("failed to get state for workflow: %w", err) } + w.Status.State = state.String() + + actions, err := r.WorkflowClient.GetActions(ctx, tinkWorkflow.GetId()) + if err != nil { + logger.Error(err, "Failed to get actions for workflow") + + return ctrl.Result{}, fmt.Errorf("failed to get actions for workflow: %w", err) + } + + statusActions := make([]tinkv1alpha1.Action, 0, len(actions)) + for _, action := range actions { + statusActions = append(statusActions, tinkv1alpha1.Action{ + Name: action.GetName(), + TaskName: action.GetTaskName(), + Image: action.GetImage(), + Timeout: action.GetTimeout(), + Command: action.GetCommand(), + OnTimeout: action.GetOnTimeout(), + OnFailure: action.GetOnFailure(), + WorkerID: action.GetWorkerId(), + Volumes: action.GetVolumes(), + Environment: action.GetEnvironment(), + }) + } + + w.Status.Actions = statusActions + + events, err := r.WorkflowClient.GetEvents(ctx, tinkWorkflow.GetId()) + if err != nil { + logger.Error(err, "Failed to get events for workflow") + + return ctrl.Result{}, fmt.Errorf("failed to get events for workflow: %w", err) + } + + statusEvents := make([]tinkv1alpha1.Event, 0, len(events)) + for _, event := range events { + statusEvents = append(statusEvents, tinkv1alpha1.Event{ + ActionName: event.GetActionName(), + TaskName: event.GetTaskName(), + ActionStatus: event.GetActionStatus().String(), + Seconds: event.GetSeconds(), + Message: event.GetMessage(), + WorkerID: event.GetWorkerId(), + CreatedAt: metav1.NewTime(event.GetCreatedAt().AsTime()), + }) + } + + w.Status.Events = statusEvents + return ctrl.Result{}, nil } @@ -177,13 +252,8 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, w *tinkv1alpha1.Workfl logger := r.Log.WithValues("workflow", w.Name) - annotations := w.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - // If we've recorded an ID for the Workflow, then we should delete it - if id, ok := annotations[tinkv1alpha1.WorkflowIDAnnotation]; ok { + if id := w.TinkID(); id != "" { err := r.WorkflowClient.Delete(ctx, id) if err != nil && !errors.Is(err, tinkclient.ErrNotFound) { logger.Error(err, "Failed to delete workflow from Tinkerbell") From 923ef042de6586c3f7eab481042b2ad43a4190be Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 23 Dec 2020 16:29:57 -0500 Subject: [PATCH 14/15] force requeueafter when workflow state is not success --- tink/controllers/workflow/controller.go | 26 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tink/controllers/workflow/controller.go b/tink/controllers/workflow/controller.go index 7f6d3f7b..4fee6ba8 100644 --- a/tink/controllers/workflow/controller.go +++ b/tink/controllers/workflow/controller.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/go-logr/logr" tinkv1alpha1 "github.com/tinkerbell/cluster-api-provider-tinkerbell/tink/api/v1alpha1" @@ -153,15 +154,6 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, w *tinkv1alpha1.Workfl w.Status.Metadata = string(md) - state, err := r.WorkflowClient.GetState(ctx, tinkWorkflow.GetId()) - if err != nil { - logger.Error(err, "Failed to get state for workflow") - - return ctrl.Result{}, fmt.Errorf("failed to get state for workflow: %w", err) - } - - w.Status.State = state.String() - actions, err := r.WorkflowClient.GetActions(ctx, tinkWorkflow.GetId()) if err != nil { logger.Error(err, "Failed to get actions for workflow") @@ -209,6 +201,22 @@ func (r *Reconciler) reconcileStatus(ctx context.Context, w *tinkv1alpha1.Workfl w.Status.Events = statusEvents + state, err := r.WorkflowClient.GetState(ctx, tinkWorkflow.GetId()) + if err != nil { + logger.Error(err, "Failed to get state for workflow") + + return ctrl.Result{}, fmt.Errorf("failed to get state for workflow: %w", err) + } + + w.Status.State = state.String() + + if state != workflow.State_STATE_SUCCESS { + // If the workflow hasn't successfully run, requeue in + // a minute. This is to workaround the lack of events + // for workflow status + return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + } + return ctrl.Result{}, nil } From c7fa75d36c92f3550a8aa23bbd4e159758bb8900 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Wed, 23 Dec 2020 16:37:40 -0500 Subject: [PATCH 15/15] de-lint --- api/v1alpha3/doc.go | 3 +++ tink/api/v1alpha1/doc.go | 3 +++ tink/api/v1alpha1/groupversion_info.go | 3 +++ tink/sources_test/tink_events_test.go | 14 +++++++------- tink/test/utils/generators.go | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/api/v1alpha3/doc.go b/api/v1alpha3/doc.go index 4903f43c..c5ca1f8c 100644 --- a/api/v1alpha3/doc.go +++ b/api/v1alpha3/doc.go @@ -1,9 +1,12 @@ /* Copyright 2020 The Kubernetes 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. diff --git a/tink/api/v1alpha1/doc.go b/tink/api/v1alpha1/doc.go index 3f7c0025..6689da7e 100644 --- a/tink/api/v1alpha1/doc.go +++ b/tink/api/v1alpha1/doc.go @@ -1,9 +1,12 @@ /* Copyright 2020 The Kubernetes 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. diff --git a/tink/api/v1alpha1/groupversion_info.go b/tink/api/v1alpha1/groupversion_info.go index 5b7a9608..37f6ef25 100644 --- a/tink/api/v1alpha1/groupversion_info.go +++ b/tink/api/v1alpha1/groupversion_info.go @@ -1,9 +1,12 @@ /* Copyright 2020 The Kubernetes 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. diff --git a/tink/sources_test/tink_events_test.go b/tink/sources_test/tink_events_test.go index 7c43b8ff..f94bf3e3 100644 --- a/tink/sources_test/tink_events_test.go +++ b/tink/sources_test/tink_events_test.go @@ -180,7 +180,7 @@ func TestTemplateEvents(t *testing.T) { //nolint: funlen g.Consistently(eventCh).ShouldNot(Receive()) // Create the matching k8s resource - te := &tinkv1.Template{ + k8sTemplate := &tinkv1.Template{ ObjectMeta: metav1.ObjectMeta{ Name: testTemplate.Name, Annotations: map[string]string{ @@ -191,15 +191,15 @@ func TestTemplateEvents(t *testing.T) { //nolint: funlen Data: pointer.StringPtr(testTemplate.Data), }, } - g.Expect(fakeClient.Create(ctx, te)).To(Succeed()) + g.Expect(fakeClient.Create(ctx, k8sTemplate)).To(Succeed()) // Update the template testTemplate.Data = sampleTemplate g.Expect(templateClient.Update(ctx, testTemplate)).To(Succeed()) expectedEvent := event.GenericEvent{ - Meta: te, - Object: te, + Meta: k8sTemplate, + Object: k8sTemplate, } g.Eventually(eventCh).Should(Receive(BeEquivalentTo(expectedEvent))) } @@ -273,7 +273,7 @@ func TestWorkflowEvents(t *testing.T) { //nolint: funlen g.Consistently(eventCh).ShouldNot(Receive()) // Create the matching k8s resources - te := &tinkv1.Template{ + k8sTemplate := &tinkv1.Template{ ObjectMeta: metav1.ObjectMeta{ Name: testTemplate.Name, Annotations: map[string]string{ @@ -285,7 +285,7 @@ func TestWorkflowEvents(t *testing.T) { //nolint: funlen }, } - g.Expect(fakeClient.Create(ctx, te)).To(Succeed()) + g.Expect(fakeClient.Create(ctx, k8sTemplate)).To(Succeed()) h := &tinkv1.Hardware{ ObjectMeta: metav1.ObjectMeta{ @@ -306,7 +306,7 @@ func TestWorkflowEvents(t *testing.T) { //nolint: funlen }, }, Spec: tinkv1.WorkflowSpec{ - TemplateRef: te.Name, + TemplateRef: k8sTemplate.Name, HardwareRef: h.Name, }, } diff --git a/tink/test/utils/generators.go b/tink/test/utils/generators.go index 7fa90e48..778781f8 100644 --- a/tink/test/utils/generators.go +++ b/tink/test/utils/generators.go @@ -73,7 +73,7 @@ func (i *ipGetter) nextAddressFromCIDR(cidr string) (string, string, string, err gw[len(gw)-1]++ gateway := gw.String() - // Attempt to get the last address used, othewise use the + // Attempt to get the last address used, otherwise use the // gateway address as the starting point lastAddress, ok := i.addresses[cidr] if !ok {