diff --git a/Makefile.tools b/Makefile.tools index bad5aea19..294e5d510 100644 --- a/Makefile.tools +++ b/Makefile.tools @@ -30,10 +30,14 @@ golangci-lint: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.15.0; \ fi +kustomize_version ?= 3.0.2 + .PHONY: kustomize kustomize: @ if [ ! $$(which kustomize) ]; then \ - go get -u sigs.k8s.io/kustomize; \ + curl -LO https://github.com/kubernetes-sigs/kustomize/releases/download/v${kustomize_version}/kustomize_${kustomize_version}_linux_amd64; \ + chmod u+x kustomize_${kustomize_version}_linux_amd64; \ + mv kustomize_${kustomize_version}_linux_amd64 /bin/kustomize; \ fi .PHONY: snyk diff --git a/README.md b/README.md index 717e14cb3..8ed73c7e2 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,32 @@ Kubernetes resource it finds referenced within its GitTracks. If however, the non-namespaced resource clashes and is defined in another GitTrack within another namespace, Faros will ignore the resource. First owner wins. +#### Cross-Namespace Ownership + +Faros creates resources in the namespaces as defined by the resource. +A namespaced GitTrack, however, should never own cluster-scoped resources or +resources in other namespaces. + +To maintain backward-compatibility, the following flag defaults to true. +In this mode, a GitTrack can own cluster-scoped resources and resources in all +namespaces. + +``` +allow-cross-namespace-ownership=true +``` + +It is recommended to set it to `false` and create a GitTrack for each namespace +that Faros should manage. + +``` +allow-cross-namespace-ownership=false +``` + +In this mode, resources in a namespace not managed by a GitTrack will be ignored. + +Alternatively, create a ClusterGitTrack, which can own cluster-scoped resources +as well as resources in all namespaces. + #### Leader Election Faros can be run in an active-standby HA configuration using Kubernetes leader diff --git a/config/crds/faros_v1alpha1_gittrack.yaml b/config/crds/faros_v1alpha1_gittrack.yaml index 71a7e213c..fdc76f361 100644 --- a/config/crds/faros_v1alpha1_gittrack.yaml +++ b/config/crds/faros_v1alpha1_gittrack.yaml @@ -60,9 +60,14 @@ spec: the deploy secret type: string secretName: - description: SecretName is the name of the Secret object containins + description: SecretName is the name of the Secret object containing the key type: string + secretNamespace: + description: SecretNamespace is the namespace of the Secret object + containing the key. Defaults to the GitTrack's namespace. Required + for ClusterGitTrack. + type: string type: description: Type is the type of credential. Accepted values are "SSH", "HTTPBasicAuth". Defaults to "SSH". diff --git a/config/crds/faros_v1alpha2_clustergittrack.yaml b/config/crds/faros_v1alpha2_clustergittrack.yaml new file mode 100644 index 000000000..f7c11997a --- /dev/null +++ b/config/crds/faros_v1alpha2_clustergittrack.yaml @@ -0,0 +1,164 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + labels: + controller-tools.k8s.io: "1.0" + name: clustergittracks.faros.pusher.com +spec: + additionalPrinterColumns: + - JSONPath: .spec.repository + name: Repository + priority: 1 + type: string + - JSONPath: .spec.reference + name: Reference + type: string + - JSONPath: .status.objectsApplied + name: Children Created + type: integer + - JSONPath: .status.objectsDiscovered + name: Resources Discovered + type: integer + - JSONPath: .status.objectsIgnored + name: Resources Ignored + type: integer + - JSONPath: .status.objectsInSync + name: Children In Sync + type: integer + - JSONPath: .metadata.creationTimestamp + name: Age + type: date + group: faros.pusher.com + names: + kind: ClusterGitTrack + plural: clustergittracks + scope: Cluster + validation: + openAPIV3Schema: + 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/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/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + properties: + deployKey: + description: DeployKey holds a reference to an SSH key needed to access + the repository + properties: + key: + description: Key is the key within the Secret object that contains + the deploy secret + type: string + secretName: + description: SecretName is the name of the Secret object containing + the key + type: string + secretNamespace: + description: SecretNamespace is the namespace of the Secret object + containing the key. Defaults to the GitTrack's namespace. Required + for ClusterGitTrack. + type: string + type: + description: Type is the type of credential. Accepted values are + "SSH", "HTTPBasicAuth". Defaults to "SSH". + enum: + - SSH + - HTTPBasicAuth + type: string + required: + - secretName + - key + type: object + reference: + description: Reference contains the git reference this GitTrack tracks + type: string + repository: + description: Repository is the git repository URI to clone from + type: string + subPath: + description: SubPath is the subpath within the repository underneath + which files are considered + pattern: ^[a-zA-Z0-9/\-.]*$ + type: string + required: + - reference + - repository + type: object + status: + properties: + conditions: + description: Conditions are the conditions on this GitTrack + items: + properties: + lastTransitionTime: + description: LastTransitionTime of this condition + format: date-time + type: string + lastUpdateTime: + description: LastUpdateTime of this condition + format: date-time + type: string + message: + description: Message associated with this condition + type: string + reason: + description: Reason for the current status of this condition + type: string + status: + description: Status of this condition + type: string + type: + description: Type of this condition + type: string + required: + - type + - status + type: object + type: array + ignoredFiles: + description: IgnoredFiles is the list of YAML files containing invalid + k8s manifests. + type: object + objectsApplied: + description: ObjectsApplied is the number of k8s objects for which a + GitTrackObjects was created + format: int64 + type: integer + objectsDiscovered: + description: ObjectsDiscovered is the number of k8s objects found in + the repository path + format: int64 + type: integer + objectsIgnored: + description: ObjectsIgnored is the number of k8s objects found in the + repository path for which no GitTrackObject was created + format: int64 + type: integer + objectsInSync: + description: ObjectsInSync is the number of GitTrackObjects that were + successfully applied to the cluster + format: int64 + type: integer + required: + - objectsDiscovered + - objectsApplied + - objectsIgnored + - objectsInSync + type: object + version: v1alpha2 +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/rbac/manager_role.yaml b/config/rbac/manager_role.yaml index 94ede84db..53d5bd368 100644 --- a/config/rbac/manager_role.yaml +++ b/config/rbac/manager_role.yaml @@ -16,6 +16,18 @@ rules: - update - patch - delete +- apiGroups: + - faros.pusher.com + resources: + - clustergittracks + verbs: + - get + - list + - watch + - create + - update + - patch + - delete - apiGroups: - faros.pusher.com resources: diff --git a/config/samples/faros_v1alpha2_clustergittrack.yaml b/config/samples/faros_v1alpha2_clustergittrack.yaml new file mode 100644 index 000000000..5c95f57ab --- /dev/null +++ b/config/samples/faros_v1alpha2_clustergittrack.yaml @@ -0,0 +1,9 @@ +apiVersion: faros.pusher.com/v1alpha2 +kind: ClusterGitTrack +metadata: + labels: + controller-tools.k8s.io: "1.0" + name: clustergittrack-sample +spec: + # Add fields here + foo: bar diff --git a/kustomize b/kustomize new file mode 100755 index 000000000..71452158a Binary files /dev/null and b/kustomize differ diff --git a/pkg/apis/addtoscheme_faros_v1alpha2.go b/pkg/apis/addtoscheme_faros_v1alpha2.go new file mode 100644 index 000000000..b28d989f2 --- /dev/null +++ b/pkg/apis/addtoscheme_faros_v1alpha2.go @@ -0,0 +1,26 @@ +/* +Copyright 2018 Pusher Ltd. + +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 apis + +import ( + "github.com/pusher/faros/pkg/apis/faros/v1alpha2" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, v1alpha2.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/apis.go b/pkg/apis/apis.go index a6a9a55ca..03148492e 100644 --- a/pkg/apis/apis.go +++ b/pkg/apis/apis.go @@ -18,13 +18,13 @@ limitations under the License. //go:generate go run ../../vendor/k8s.io/code-generator/cmd/deepcopy-gen/main.go -O zz_generated.deepcopy -i ./... -h ../../hack/boilerplate.go.txt // Generate clientset for apis -//go:generate go run ../../vendor/k8s.io/code-generator/cmd/client-gen/main.go --input-base=github.com/pusher/faros/pkg/apis --input="faros/v1alpha1" -n clientset -p github.com/pusher/faros/pkg/client -h ../../hack/boilerplate.go.txt +//go:generate go run ../../vendor/k8s.io/code-generator/cmd/client-gen/main.go --input-base=github.com/pusher/faros/pkg/apis --input="faros/v1alpha1" --input="faros/v1alpha2" -n clientset -p github.com/pusher/faros/pkg/client -h ../../hack/boilerplate.go.txt // Generate listers for apis -//go:generate go run ../../vendor/k8s.io/code-generator/cmd/lister-gen/main.go --input-dirs=github.com/pusher/faros/pkg/apis/faros/v1alpha1 -p github.com/pusher/faros/pkg/client/listers -h ../../hack/boilerplate.go.txt +//go:generate go run ../../vendor/k8s.io/code-generator/cmd/lister-gen/main.go --input-dirs=github.com/pusher/faros/pkg/apis/faros/v1alpha1,github.com/pusher/faros/pkg/apis/faros/v1alpha2 -p github.com/pusher/faros/pkg/client/listers -h ../../hack/boilerplate.go.txt -// Generate infromers for apis -//go:generate go run ../../vendor/k8s.io/code-generator/cmd/informer-gen/main.go --input-dirs=github.com/pusher/faros/pkg/apis/faros/v1alpha1 -p github.com/pusher/faros/pkg/client/informers --listers-package github.com/pusher/faros/pkg/client/listers --versioned-clientset-package github.com/pusher/faros/pkg/client/clientset -h ../../hack/boilerplate.go.txt +// Generate informers for apis +//go:generate go run ../../vendor/k8s.io/code-generator/cmd/informer-gen/main.go --input-dirs=github.com/pusher/faros/pkg/apis/faros/v1alpha1,github.com/pusher/faros/pkg/apis/faros/v1alpha2 -p github.com/pusher/faros/pkg/client/informers --listers-package github.com/pusher/faros/pkg/client/listers --versioned-clientset-package github.com/pusher/faros/pkg/client/clientset -h ../../hack/boilerplate.go.txt // Package apis contains Kubernetes API groups. package apis diff --git a/pkg/apis/faros/v1alpha1/gittrack_types.go b/pkg/apis/faros/v1alpha1/gittrack_types.go index 6bfcee469..40aadeebb 100644 --- a/pkg/apis/faros/v1alpha1/gittrack_types.go +++ b/pkg/apis/faros/v1alpha1/gittrack_types.go @@ -17,6 +17,8 @@ limitations under the License. package v1alpha1 import ( + "fmt" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -49,9 +51,12 @@ type GitTrackSpec struct { // GitTrackDeployKey holds a reference to a secret such as an SSH key or HTTP Basic Auth credentials needed to access the repository type GitTrackDeployKey struct { - // SecretName is the name of the Secret object containins the key + // SecretName is the name of the Secret object containing the key SecretName string `json:"secretName"` + // SecretNamespace is the namespace of the Secret object containing the key. Defaults to the GitTrack's namespace. Required for ClusterGitTrack. + SecretNamespace string `json:"secretNamespace,omitempty"` + // Key is the key within the Secret object that contains the deploy secret Key string `json:"key"` @@ -142,6 +147,36 @@ type GitTrack struct { Status GitTrackStatus `json:"status,omitempty"` } +// GetNamespacedName implementes the GitTrack interface +func (g *GitTrack) GetNamespacedName() string { + return fmt.Sprintf("%s/%s", g.Namespace, g.Name) +} + +// GetSpec implements the GitTrack interface +func (g *GitTrack) GetSpec() GitTrackSpec { + return g.Spec +} + +// SetSpec implements the GitTrack interface +func (g *GitTrack) SetSpec(s GitTrackSpec) { + g.Spec = s +} + +// GetStatus implements the GitTrack interface +func (g *GitTrack) GetStatus() GitTrackStatus { + return g.Status +} + +// SetStatus implements the GitTrack interface +func (g *GitTrack) SetStatus(s GitTrackStatus) { + g.Status = s +} + +// DeepCopyInterface implements the GitTrack interface +func (g *GitTrack) DeepCopyInterface() GitTrackInterface { + return g.DeepCopy() +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // GitTrackList contains a list of GitTrack diff --git a/pkg/apis/faros/v1alpha1/interfaces.go b/pkg/apis/faros/v1alpha1/interfaces.go index 3c78f02e5..e5b24bc65 100644 --- a/pkg/apis/faros/v1alpha1/interfaces.go +++ b/pkg/apis/faros/v1alpha1/interfaces.go @@ -22,6 +22,21 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +// GitTrackInterface represents an interface implemented by both +// GitTrack and ClusterGitTrack to allow them to be passed +// interchangably. +type GitTrackInterface interface { + runtime.Object + v1.Object + schema.ObjectKind + GetNamespacedName() string + GetSpec() GitTrackSpec + SetSpec(GitTrackSpec) + GetStatus() GitTrackStatus + SetStatus(GitTrackStatus) + DeepCopyInterface() GitTrackInterface +} + // GitTrackObjectInterface represents an interface implemented by both // GitTrackObject and ClusterGitTrackObject to allow them to be passed // interchangably. diff --git a/pkg/apis/faros/v1alpha2/clustergittrack_types.go b/pkg/apis/faros/v1alpha2/clustergittrack_types.go new file mode 100644 index 000000000..2f75ff454 --- /dev/null +++ b/pkg/apis/faros/v1alpha2/clustergittrack_types.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 Pusher Ltd. + +Licensed under the Apache License, Version 3.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 v1alpha2 + +import ( + "fmt" + + v1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient:nonNamespaced + +// ClusterGitTrack is the Schema for the clustergittracks API +// +k8s:openapi-gen=true +// +kubebuilder:printcolumn:name="Repository",type="string",JSONPath=".spec.repository",priority=1 +// +kubebuilder:printcolumn:name="Reference",type="string",JSONPath=".spec.reference" +// +kubebuilder:printcolumn:name="Children Created",type="integer",JSONPath=".status.objectsApplied" +// +kubebuilder:printcolumn:name="Resources Discovered",type="integer",JSONPath=".status.objectsDiscovered" +// +kubebuilder:printcolumn:name="Resources Ignored",type="integer",JSONPath=".status.objectsIgnored" +// +kubebuilder:printcolumn:name="Children In Sync",type="integer",JSONPath=".status.objectsInSync" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type ClusterGitTrack struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec v1alpha1.GitTrackSpec `json:"spec,omitempty"` + Status v1alpha1.GitTrackStatus `json:"status,omitempty"` +} + +// GetNamespacedName implementes the GitTrack interface +func (g *ClusterGitTrack) GetNamespacedName() string { + return fmt.Sprintf("%s/%s", g.Namespace, g.Name) +} + +// GetSpec implements the GitTrack interface +func (g *ClusterGitTrack) GetSpec() v1alpha1.GitTrackSpec { + return g.Spec +} + +// SetSpec implements the GitTrack interface +func (g *ClusterGitTrack) SetSpec(s v1alpha1.GitTrackSpec) { + g.Spec = s +} + +// GetStatus implements the GitTrack interface +func (g *ClusterGitTrack) GetStatus() v1alpha1.GitTrackStatus { + return g.Status +} + +// SetStatus implements the GitTrack interface +func (g *ClusterGitTrack) SetStatus(s v1alpha1.GitTrackStatus) { + g.Status = s +} + +// DeepCopyInterface implements the GitTrack interface +func (g *ClusterGitTrack) DeepCopyInterface() v1alpha1.GitTrackInterface { + return g.DeepCopy() +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +genclient:nonNamespaced + +// ClusterGitTrackList contains a list of ClusterGitTrack +type ClusterGitTrackList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterGitTrack `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterGitTrack{}, &ClusterGitTrackList{}) +} diff --git a/pkg/apis/faros/v1alpha2/clustergittrack_types_test.go b/pkg/apis/faros/v1alpha2/clustergittrack_types_test.go new file mode 100644 index 000000000..c88dc14de --- /dev/null +++ b/pkg/apis/faros/v1alpha2/clustergittrack_types_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2018 Pusher Ltd. + +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 v1alpha2 + +import ( + "testing" + + v1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + + "github.com/onsi/gomega" + "golang.org/x/net/context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestStorageClusterGitTrack(t *testing.T) { + key := types.NamespacedName{Name: "foo"} + created := &ClusterGitTrack{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + g := gomega.NewGomegaWithT(t) + + // Test Create + fetched := &ClusterGitTrack{} + g.Expect(c.Create(context.TODO(), created)).NotTo(gomega.HaveOccurred()) + + g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(fetched).To(gomega.Equal(created)) + + // Test Updating the Labels + updated := fetched.DeepCopy() + updated.Labels = map[string]string{"hello": "world"} + g.Expect(c.Update(context.TODO(), updated)).NotTo(gomega.HaveOccurred()) + + g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(fetched).To(gomega.Equal(updated)) + + // Test Setting deployKey + keyless := fetched.DeepCopy() + keyless.Spec.DeployKey = v1alpha1.GitTrackDeployKey{ + SecretName: "secretfoo", + Key: "secretbar", + } + g.Expect(c.Update(context.TODO(), keyless)).NotTo(gomega.HaveOccurred()) + g.Expect(c.Get(context.TODO(), key, fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(fetched).To(gomega.Equal(keyless)) + + // Test Delete + g.Expect(c.Delete(context.TODO(), fetched)).NotTo(gomega.HaveOccurred()) + g.Expect(c.Get(context.TODO(), key, fetched)).To(gomega.HaveOccurred()) +} diff --git a/pkg/apis/faros/v1alpha2/doc.go b/pkg/apis/faros/v1alpha2/doc.go new file mode 100644 index 000000000..f03a8fe4d --- /dev/null +++ b/pkg/apis/faros/v1alpha2/doc.go @@ -0,0 +1,23 @@ +/* +Copyright 2018 Pusher Ltd. + +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 v1alpha2 contains API Schema definitions for the faros v1alpha2 API group +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=github.com/pusher/faros/pkg/apis/faros +// +k8s:defaulter-gen=TypeMeta +// +groupName=faros.pusher.com +package v1alpha2 diff --git a/pkg/apis/faros/v1alpha2/meta.go b/pkg/apis/faros/v1alpha2/meta.go new file mode 100644 index 000000000..96b5dae4f --- /dev/null +++ b/pkg/apis/faros/v1alpha2/meta.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 Pusher Ltd. + +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 v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// API related string constants for Group, Version and Kinds within +// v1alpha2.faros.pusher.com +const ( + Group = "faros.pusher.com" + Version = "v1alpha2" + + ClusterGitTrackKind = "ClusterGitTrack" +) + +// GroupVersion and TypeMeta for v1alpha2.faros.pusher.com +var ( + GroupVersion = schema.GroupVersion{ + Group: Group, + Version: Version, + } + + ClusterGitTrackTypeMeta = metav1.TypeMeta{ + APIVersion: GroupVersion.String(), + Kind: ClusterGitTrackKind, + } +) diff --git a/pkg/apis/faros/v1alpha2/register.go b/pkg/apis/faros/v1alpha2/register.go new file mode 100644 index 000000000..86e441e60 --- /dev/null +++ b/pkg/apis/faros/v1alpha2/register.go @@ -0,0 +1,46 @@ +/* +Copyright 2018 Pusher Ltd. + +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. +*/ + +// NOTE: Boilerplate only. Ignore this file. + +// Package v1alpha2 contains API Schema definitions for the faros v1alpha2 API group +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=github.com/pusher/faros/pkg/apis/faros +// +k8s:defaulter-gen=TypeMeta +// +groupName=faros.pusher.com +package v1alpha2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: "faros.pusher.com", Version: "v1alpha2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} + + // AddToScheme is used to register the new types with the Scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/faros/v1alpha2/v1alpha2_suite_test.go b/pkg/apis/faros/v1alpha2/v1alpha2_suite_test.go new file mode 100644 index 000000000..9f44a4942 --- /dev/null +++ b/pkg/apis/faros/v1alpha2/v1alpha2_suite_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2018 Pusher Ltd. + +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 v1alpha2 + +import ( + "log" + "os" + "path/filepath" + "testing" + + _ "github.com/pusher/faros/test/reporters" // Not using ginkgo here but need the flags set from this package + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var cfg *rest.Config +var c client.Client + +func TestMain(m *testing.M) { + t := &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crds")}, + } + + err := SchemeBuilder.AddToScheme(scheme.Scheme) + if err != nil { + log.Fatal(err) + } + + if cfg, err = t.Start(); err != nil { + log.Fatal(err) + } + + if c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}); err != nil { + log.Fatal(err) + } + + code := m.Run() + t.Stop() + os.Exit(code) +} diff --git a/pkg/apis/faros/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/faros/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 000000000..94d49714a --- /dev/null +++ b/pkg/apis/faros/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,86 @@ +// +build !ignore_autogenerated + +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +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 *ClusterGitTrack) DeepCopyInto(out *ClusterGitTrack) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGitTrack. +func (in *ClusterGitTrack) DeepCopy() *ClusterGitTrack { + if in == nil { + return nil + } + out := new(ClusterGitTrack) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterGitTrack) 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 *ClusterGitTrackList) DeepCopyInto(out *ClusterGitTrackList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterGitTrack, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGitTrackList. +func (in *ClusterGitTrackList) DeepCopy() *ClusterGitTrackList { + if in == nil { + return nil + } + out := new(ClusterGitTrackList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterGitTrackList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/pkg/client/clientset/clientset.go b/pkg/client/clientset/clientset.go index 0fd159958..27e4e8f7c 100644 --- a/pkg/client/clientset/clientset.go +++ b/pkg/client/clientset/clientset.go @@ -20,6 +20,7 @@ package clientset import ( farosv1alpha1 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha1" + farosv1alpha2 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha2" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" @@ -30,6 +31,7 @@ type Interface interface { FarosV1alpha1() farosv1alpha1.FarosV1alpha1Interface // Deprecated: please explicitly pick a version if possible. Faros() farosv1alpha1.FarosV1alpha1Interface + FarosV1alpha2() farosv1alpha2.FarosV1alpha2Interface } // Clientset contains the clients for groups. Each group has exactly one @@ -37,6 +39,7 @@ type Interface interface { type Clientset struct { *discovery.DiscoveryClient farosV1alpha1 *farosv1alpha1.FarosV1alpha1Client + farosV1alpha2 *farosv1alpha2.FarosV1alpha2Client } // FarosV1alpha1 retrieves the FarosV1alpha1Client @@ -50,6 +53,11 @@ func (c *Clientset) Faros() farosv1alpha1.FarosV1alpha1Interface { return c.farosV1alpha1 } +// FarosV1alpha2 retrieves the FarosV1alpha2Client +func (c *Clientset) FarosV1alpha2() farosv1alpha2.FarosV1alpha2Interface { + return c.farosV1alpha2 +} + // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { @@ -70,6 +78,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { if err != nil { return nil, err } + cs.farosV1alpha2, err = farosv1alpha2.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) if err != nil { @@ -83,6 +95,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset cs.farosV1alpha1 = farosv1alpha1.NewForConfigOrDie(c) + cs.farosV1alpha2 = farosv1alpha2.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) return &cs @@ -92,6 +105,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { func New(c rest.Interface) *Clientset { var cs Clientset cs.farosV1alpha1 = farosv1alpha1.New(c) + cs.farosV1alpha2 = farosv1alpha2.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs diff --git a/pkg/client/clientset/fake/clientset_generated.go b/pkg/client/clientset/fake/clientset_generated.go index cc960a2af..f05b87ad4 100644 --- a/pkg/client/clientset/fake/clientset_generated.go +++ b/pkg/client/clientset/fake/clientset_generated.go @@ -22,6 +22,8 @@ import ( clientset "github.com/pusher/faros/pkg/client/clientset" farosv1alpha1 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha1" fakefarosv1alpha1 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha1/fake" + farosv1alpha2 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha2" + fakefarosv1alpha2 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha2/fake" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" @@ -80,3 +82,8 @@ func (c *Clientset) FarosV1alpha1() farosv1alpha1.FarosV1alpha1Interface { func (c *Clientset) Faros() farosv1alpha1.FarosV1alpha1Interface { return &fakefarosv1alpha1.FakeFarosV1alpha1{Fake: &c.Fake} } + +// FarosV1alpha2 retrieves the FarosV1alpha2Client +func (c *Clientset) FarosV1alpha2() farosv1alpha2.FarosV1alpha2Interface { + return &fakefarosv1alpha2.FakeFarosV1alpha2{Fake: &c.Fake} +} diff --git a/pkg/client/clientset/fake/register.go b/pkg/client/clientset/fake/register.go index ed72ef73e..fc5de9245 100644 --- a/pkg/client/clientset/fake/register.go +++ b/pkg/client/clientset/fake/register.go @@ -20,6 +20,7 @@ package fake import ( farosv1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + farosv1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -32,6 +33,7 @@ var codecs = serializer.NewCodecFactory(scheme) var parameterCodec = runtime.NewParameterCodec(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ farosv1alpha1.AddToScheme, + farosv1alpha2.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/scheme/register.go b/pkg/client/clientset/scheme/register.go index a0015b554..9e3045428 100644 --- a/pkg/client/clientset/scheme/register.go +++ b/pkg/client/clientset/scheme/register.go @@ -20,6 +20,7 @@ package scheme import ( farosv1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + farosv1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" @@ -32,6 +33,7 @@ var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ farosv1alpha1.AddToScheme, + farosv1alpha2.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition diff --git a/pkg/client/clientset/typed/faros/v1alpha2/clustergittrack.go b/pkg/client/clientset/typed/faros/v1alpha2/clustergittrack.go new file mode 100644 index 000000000..8aa254449 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/clustergittrack.go @@ -0,0 +1,180 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +import ( + "time" + + v1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" + scheme "github.com/pusher/faros/pkg/client/clientset/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ClusterGitTracksGetter has a method to return a ClusterGitTrackInterface. +// A group's client should implement this interface. +type ClusterGitTracksGetter interface { + ClusterGitTracks() ClusterGitTrackInterface +} + +// ClusterGitTrackInterface has methods to work with ClusterGitTrack resources. +type ClusterGitTrackInterface interface { + Create(*v1alpha2.ClusterGitTrack) (*v1alpha2.ClusterGitTrack, error) + Update(*v1alpha2.ClusterGitTrack) (*v1alpha2.ClusterGitTrack, error) + UpdateStatus(*v1alpha2.ClusterGitTrack) (*v1alpha2.ClusterGitTrack, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha2.ClusterGitTrack, error) + List(opts v1.ListOptions) (*v1alpha2.ClusterGitTrackList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.ClusterGitTrack, err error) + ClusterGitTrackExpansion +} + +// clusterGitTracks implements ClusterGitTrackInterface +type clusterGitTracks struct { + client rest.Interface +} + +// newClusterGitTracks returns a ClusterGitTracks +func newClusterGitTracks(c *FarosV1alpha2Client) *clusterGitTracks { + return &clusterGitTracks{ + client: c.RESTClient(), + } +} + +// Get takes name of the clusterGitTrack, and returns the corresponding clusterGitTrack object, and an error if there is any. +func (c *clusterGitTracks) Get(name string, options v1.GetOptions) (result *v1alpha2.ClusterGitTrack, err error) { + result = &v1alpha2.ClusterGitTrack{} + err = c.client.Get(). + Resource("clustergittracks"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ClusterGitTracks that match those selectors. +func (c *clusterGitTracks) List(opts v1.ListOptions) (result *v1alpha2.ClusterGitTrackList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.ClusterGitTrackList{} + err = c.client.Get(). + Resource("clustergittracks"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested clusterGitTracks. +func (c *clusterGitTracks) Watch(opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("clustergittracks"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a clusterGitTrack and creates it. Returns the server's representation of the clusterGitTrack, and an error, if there is any. +func (c *clusterGitTracks) Create(clusterGitTrack *v1alpha2.ClusterGitTrack) (result *v1alpha2.ClusterGitTrack, err error) { + result = &v1alpha2.ClusterGitTrack{} + err = c.client.Post(). + Resource("clustergittracks"). + Body(clusterGitTrack). + Do(). + Into(result) + return +} + +// Update takes the representation of a clusterGitTrack and updates it. Returns the server's representation of the clusterGitTrack, and an error, if there is any. +func (c *clusterGitTracks) Update(clusterGitTrack *v1alpha2.ClusterGitTrack) (result *v1alpha2.ClusterGitTrack, err error) { + result = &v1alpha2.ClusterGitTrack{} + err = c.client.Put(). + Resource("clustergittracks"). + Name(clusterGitTrack.Name). + Body(clusterGitTrack). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *clusterGitTracks) UpdateStatus(clusterGitTrack *v1alpha2.ClusterGitTrack) (result *v1alpha2.ClusterGitTrack, err error) { + result = &v1alpha2.ClusterGitTrack{} + err = c.client.Put(). + Resource("clustergittracks"). + Name(clusterGitTrack.Name). + SubResource("status"). + Body(clusterGitTrack). + Do(). + Into(result) + return +} + +// Delete takes name of the clusterGitTrack and deletes it. Returns an error if one occurs. +func (c *clusterGitTracks) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Resource("clustergittracks"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *clusterGitTracks) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("clustergittracks"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched clusterGitTrack. +func (c *clusterGitTracks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.ClusterGitTrack, err error) { + result = &v1alpha2.ClusterGitTrack{} + err = c.client.Patch(pt). + Resource("clustergittracks"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/clientset/typed/faros/v1alpha2/doc.go b/pkg/client/clientset/typed/faros/v1alpha2/doc.go new file mode 100644 index 000000000..1bf8c4a99 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha2 diff --git a/pkg/client/clientset/typed/faros/v1alpha2/fake/doc.go b/pkg/client/clientset/typed/faros/v1alpha2/fake/doc.go new file mode 100644 index 000000000..1828823a0 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/fake/doc.go @@ -0,0 +1,20 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/client/clientset/typed/faros/v1alpha2/fake/fake_clustergittrack.go b/pkg/client/clientset/typed/faros/v1alpha2/fake/fake_clustergittrack.go new file mode 100644 index 000000000..82c7eb0c5 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/fake/fake_clustergittrack.go @@ -0,0 +1,131 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeClusterGitTracks implements ClusterGitTrackInterface +type FakeClusterGitTracks struct { + Fake *FakeFarosV1alpha2 +} + +var clustergittracksResource = schema.GroupVersionResource{Group: "faros.pusher.com", Version: "v1alpha2", Resource: "clustergittracks"} + +var clustergittracksKind = schema.GroupVersionKind{Group: "faros.pusher.com", Version: "v1alpha2", Kind: "ClusterGitTrack"} + +// Get takes name of the clusterGitTrack, and returns the corresponding clusterGitTrack object, and an error if there is any. +func (c *FakeClusterGitTracks) Get(name string, options v1.GetOptions) (result *v1alpha2.ClusterGitTrack, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(clustergittracksResource, name), &v1alpha2.ClusterGitTrack{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterGitTrack), err +} + +// List takes label and field selectors, and returns the list of ClusterGitTracks that match those selectors. +func (c *FakeClusterGitTracks) List(opts v1.ListOptions) (result *v1alpha2.ClusterGitTrackList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(clustergittracksResource, clustergittracksKind, opts), &v1alpha2.ClusterGitTrackList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.ClusterGitTrackList{ListMeta: obj.(*v1alpha2.ClusterGitTrackList).ListMeta} + for _, item := range obj.(*v1alpha2.ClusterGitTrackList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested clusterGitTracks. +func (c *FakeClusterGitTracks) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(clustergittracksResource, opts)) +} + +// Create takes the representation of a clusterGitTrack and creates it. Returns the server's representation of the clusterGitTrack, and an error, if there is any. +func (c *FakeClusterGitTracks) Create(clusterGitTrack *v1alpha2.ClusterGitTrack) (result *v1alpha2.ClusterGitTrack, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(clustergittracksResource, clusterGitTrack), &v1alpha2.ClusterGitTrack{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterGitTrack), err +} + +// Update takes the representation of a clusterGitTrack and updates it. Returns the server's representation of the clusterGitTrack, and an error, if there is any. +func (c *FakeClusterGitTracks) Update(clusterGitTrack *v1alpha2.ClusterGitTrack) (result *v1alpha2.ClusterGitTrack, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(clustergittracksResource, clusterGitTrack), &v1alpha2.ClusterGitTrack{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterGitTrack), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeClusterGitTracks) UpdateStatus(clusterGitTrack *v1alpha2.ClusterGitTrack) (*v1alpha2.ClusterGitTrack, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(clustergittracksResource, "status", clusterGitTrack), &v1alpha2.ClusterGitTrack{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterGitTrack), err +} + +// Delete takes name of the clusterGitTrack and deletes it. Returns an error if one occurs. +func (c *FakeClusterGitTracks) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteAction(clustergittracksResource, name), &v1alpha2.ClusterGitTrack{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeClusterGitTracks) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(clustergittracksResource, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha2.ClusterGitTrackList{}) + return err +} + +// Patch applies the patch and returns the patched clusterGitTrack. +func (c *FakeClusterGitTracks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.ClusterGitTrack, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(clustergittracksResource, name, pt, data, subresources...), &v1alpha2.ClusterGitTrack{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterGitTrack), err +} diff --git a/pkg/client/clientset/typed/faros/v1alpha2/fake/fake_faros_client.go b/pkg/client/clientset/typed/faros/v1alpha2/fake/fake_faros_client.go new file mode 100644 index 000000000..3f186c9b1 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/fake/fake_faros_client.go @@ -0,0 +1,40 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package fake + +import ( + v1alpha2 "github.com/pusher/faros/pkg/client/clientset/typed/faros/v1alpha2" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeFarosV1alpha2 struct { + *testing.Fake +} + +func (c *FakeFarosV1alpha2) ClusterGitTracks() v1alpha2.ClusterGitTrackInterface { + return &FakeClusterGitTracks{c} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeFarosV1alpha2) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/client/clientset/typed/faros/v1alpha2/faros_client.go b/pkg/client/clientset/typed/faros/v1alpha2/faros_client.go new file mode 100644 index 000000000..76f199015 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/faros_client.go @@ -0,0 +1,90 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" + "github.com/pusher/faros/pkg/client/clientset/scheme" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + rest "k8s.io/client-go/rest" +) + +type FarosV1alpha2Interface interface { + RESTClient() rest.Interface + ClusterGitTracksGetter +} + +// FarosV1alpha2Client is used to interact with features provided by the faros.pusher.com group. +type FarosV1alpha2Client struct { + restClient rest.Interface +} + +func (c *FarosV1alpha2Client) ClusterGitTracks() ClusterGitTrackInterface { + return newClusterGitTracks(c) +} + +// NewForConfig creates a new FarosV1alpha2Client for the given config. +func NewForConfig(c *rest.Config) (*FarosV1alpha2Client, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &FarosV1alpha2Client{client}, nil +} + +// NewForConfigOrDie creates a new FarosV1alpha2Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *FarosV1alpha2Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new FarosV1alpha2Client for the given RESTClient. +func New(c rest.Interface) *FarosV1alpha2Client { + return &FarosV1alpha2Client{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha2.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FarosV1alpha2Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/client/clientset/typed/faros/v1alpha2/generated_expansion.go b/pkg/client/clientset/typed/faros/v1alpha2/generated_expansion.go new file mode 100644 index 000000000..b9e659a67 --- /dev/null +++ b/pkg/client/clientset/typed/faros/v1alpha2/generated_expansion.go @@ -0,0 +1,21 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +type ClusterGitTrackExpansion interface{} diff --git a/pkg/client/informers/externalversions/faros/interface.go b/pkg/client/informers/externalversions/faros/interface.go index f75750870..90e67b963 100644 --- a/pkg/client/informers/externalversions/faros/interface.go +++ b/pkg/client/informers/externalversions/faros/interface.go @@ -20,6 +20,7 @@ package faros import ( v1alpha1 "github.com/pusher/faros/pkg/client/informers/externalversions/faros/v1alpha1" + v1alpha2 "github.com/pusher/faros/pkg/client/informers/externalversions/faros/v1alpha2" internalinterfaces "github.com/pusher/faros/pkg/client/informers/externalversions/internalinterfaces" ) @@ -27,6 +28,8 @@ import ( type Interface interface { // V1alpha1 provides access to shared informers for resources in V1alpha1. V1alpha1() v1alpha1.Interface + // V1alpha2 provides access to shared informers for resources in V1alpha2. + V1alpha2() v1alpha2.Interface } type group struct { @@ -44,3 +47,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (g *group) V1alpha1() v1alpha1.Interface { return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) } + +// V1alpha2 returns a new v1alpha2.Interface. +func (g *group) V1alpha2() v1alpha2.Interface { + return v1alpha2.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/client/informers/externalversions/faros/v1alpha2/clustergittrack.go b/pkg/client/informers/externalversions/faros/v1alpha2/clustergittrack.go new file mode 100644 index 000000000..120d04edb --- /dev/null +++ b/pkg/client/informers/externalversions/faros/v1alpha2/clustergittrack.go @@ -0,0 +1,88 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +import ( + time "time" + + farosv1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" + clientset "github.com/pusher/faros/pkg/client/clientset" + internalinterfaces "github.com/pusher/faros/pkg/client/informers/externalversions/internalinterfaces" + v1alpha2 "github.com/pusher/faros/pkg/client/listers/faros/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ClusterGitTrackInformer provides access to a shared informer and lister for +// ClusterGitTracks. +type ClusterGitTrackInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.ClusterGitTrackLister +} + +type clusterGitTrackInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewClusterGitTrackInformer constructs a new informer for ClusterGitTrack type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewClusterGitTrackInformer(client clientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredClusterGitTrackInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredClusterGitTrackInformer constructs a new informer for ClusterGitTrack type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredClusterGitTrackInformer(client clientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.FarosV1alpha2().ClusterGitTracks().List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.FarosV1alpha2().ClusterGitTracks().Watch(options) + }, + }, + &farosv1alpha2.ClusterGitTrack{}, + resyncPeriod, + indexers, + ) +} + +func (f *clusterGitTrackInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredClusterGitTrackInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *clusterGitTrackInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&farosv1alpha2.ClusterGitTrack{}, f.defaultInformer) +} + +func (f *clusterGitTrackInformer) Lister() v1alpha2.ClusterGitTrackLister { + return v1alpha2.NewClusterGitTrackLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/faros/v1alpha2/interface.go b/pkg/client/informers/externalversions/faros/v1alpha2/interface.go new file mode 100644 index 000000000..9d5b08186 --- /dev/null +++ b/pkg/client/informers/externalversions/faros/v1alpha2/interface.go @@ -0,0 +1,45 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +import ( + internalinterfaces "github.com/pusher/faros/pkg/client/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // ClusterGitTracks returns a ClusterGitTrackInformer. + ClusterGitTracks() ClusterGitTrackInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// ClusterGitTracks returns a ClusterGitTrackInformer. +func (v *version) ClusterGitTracks() ClusterGitTrackInformer { + return &clusterGitTrackInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index ef2007488..f3908d8be 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -22,6 +22,7 @@ import ( "fmt" v1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + v1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) @@ -60,6 +61,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case v1alpha1.SchemeGroupVersion.WithResource("gittrackobjects"): return &genericInformer{resource: resource.GroupResource(), informer: f.Faros().V1alpha1().GitTrackObjects().Informer()}, nil + // Group=faros.pusher.com, Version=v1alpha2 + case v1alpha2.SchemeGroupVersion.WithResource("clustergittracks"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Faros().V1alpha2().ClusterGitTracks().Informer()}, nil + } return nil, fmt.Errorf("no informer found for %v", resource) diff --git a/pkg/client/listers/faros/v1alpha2/clustergittrack.go b/pkg/client/listers/faros/v1alpha2/clustergittrack.go new file mode 100644 index 000000000..c4ebcdf8e --- /dev/null +++ b/pkg/client/listers/faros/v1alpha2/clustergittrack.go @@ -0,0 +1,65 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ClusterGitTrackLister helps list ClusterGitTracks. +type ClusterGitTrackLister interface { + // List lists all ClusterGitTracks in the indexer. + List(selector labels.Selector) (ret []*v1alpha2.ClusterGitTrack, err error) + // Get retrieves the ClusterGitTrack from the index for a given name. + Get(name string) (*v1alpha2.ClusterGitTrack, error) + ClusterGitTrackListerExpansion +} + +// clusterGitTrackLister implements the ClusterGitTrackLister interface. +type clusterGitTrackLister struct { + indexer cache.Indexer +} + +// NewClusterGitTrackLister returns a new ClusterGitTrackLister. +func NewClusterGitTrackLister(indexer cache.Indexer) ClusterGitTrackLister { + return &clusterGitTrackLister{indexer: indexer} +} + +// List lists all ClusterGitTracks in the indexer. +func (s *clusterGitTrackLister) List(selector labels.Selector) (ret []*v1alpha2.ClusterGitTrack, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.ClusterGitTrack)) + }) + return ret, err +} + +// Get retrieves the ClusterGitTrack from the index for a given name. +func (s *clusterGitTrackLister) Get(name string) (*v1alpha2.ClusterGitTrack, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("clustergittrack"), name) + } + return obj.(*v1alpha2.ClusterGitTrack), nil +} diff --git a/pkg/client/listers/faros/v1alpha2/expansion_generated.go b/pkg/client/listers/faros/v1alpha2/expansion_generated.go new file mode 100644 index 000000000..a49a8e95c --- /dev/null +++ b/pkg/client/listers/faros/v1alpha2/expansion_generated.go @@ -0,0 +1,23 @@ +/* +Copyright 2018 Pusher Ltd. + +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 main. DO NOT EDIT. + +package v1alpha2 + +// ClusterGitTrackListerExpansion allows custom methods to be added to +// ClusterGitTrackLister. +type ClusterGitTrackListerExpansion interface{} diff --git a/pkg/controller/gittrack/gittrack_controller.go b/pkg/controller/gittrack/gittrack_controller.go index 15305c615..2c605eddc 100644 --- a/pkg/controller/gittrack/gittrack_controller.go +++ b/pkg/controller/gittrack/gittrack_controller.go @@ -25,6 +25,7 @@ import ( "github.com/go-logr/logr" farosv1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + farosv1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" gittrackutils "github.com/pusher/faros/pkg/controller/gittrack/utils" farosflags "github.com/pusher/faros/pkg/flags" utils "github.com/pusher/faros/pkg/utils" @@ -103,6 +104,11 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } + err = c.Watch(&source.Kind{Type: &farosv1alpha2.ClusterGitTrack{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + err = c.Watch(&source.Kind{Type: &farosv1alpha1.GitTrackObject{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &farosv1alpha1.GitTrack{}, @@ -119,6 +125,22 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } + err = c.Watch(&source.Kind{Type: &farosv1alpha1.GitTrackObject{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &farosv1alpha2.ClusterGitTrack{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &farosv1alpha1.ClusterGitTrackObject{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &farosv1alpha2.ClusterGitTrack{}, + }) + if err != nil { + return err + } + return nil } @@ -208,21 +230,25 @@ func (r *ReconcileGitTrack) fetchGitCredentials(namespace string, deployKey faro // getFiles checks out the Spec.Repository at Spec.Reference and returns a map of filename to // gitstore.File pointers -func (r *ReconcileGitTrack) getFiles(gt *farosv1alpha1.GitTrack) (map[string]*gitstore.File, error) { - r.recorder.Eventf(gt, apiv1.EventTypeNormal, "CheckoutStarted", "Checking out '%s' at '%s'", gt.Spec.Repository, gt.Spec.Reference) - gitCreds, err := r.fetchGitCredentials(gt.Namespace, gt.Spec.DeployKey) +func (r *ReconcileGitTrack) getFiles(gt farosv1alpha1.GitTrackInterface) (map[string]*gitstore.File, error) { + r.recorder.Eventf(gt, apiv1.EventTypeNormal, "CheckoutStarted", "Checking out '%s' at '%s'", gt.GetSpec().Repository, gt.GetSpec().Reference) + namespace := gt.GetNamespace() + if gt.GetSpec().DeployKey.SecretNamespace != "" { + namespace = gt.GetSpec().DeployKey.SecretNamespace + } + gitCreds, err := r.fetchGitCredentials(namespace, gt.GetSpec().DeployKey) if err != nil { - r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "Failed to checkout '%s' at '%s'", gt.Spec.Repository, gt.Spec.Reference) + r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "Failed to checkout '%s' at '%s'", gt.GetSpec().Repository, gt.GetSpec().Reference) return nil, fmt.Errorf("unable to retrieve git credentials from secret: %v", err) } - repo, err := r.checkoutRepo(gt.Spec.Repository, gt.Spec.Reference, gitCreds) + repo, err := r.checkoutRepo(gt.GetSpec().Repository, gt.GetSpec().Reference, gitCreds) if err != nil { - r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "Failed to checkout '%s' at '%s'", gt.Spec.Repository, gt.Spec.Reference) + r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "Failed to checkout '%s' at '%s'", gt.GetSpec().Repository, gt.GetSpec().Reference) return nil, err } - subPath := gt.Spec.SubPath + subPath := gt.GetSpec().SubPath if !strings.HasSuffix(subPath, "/") { subPath += "/" } @@ -231,11 +257,11 @@ func (r *ReconcileGitTrack) getFiles(gt *farosv1alpha1.GitTrack) (map[string]*gi globbedSubPath := strings.TrimPrefix(subPath, "/") + "{**/*,*}.{yaml,yml,json}" files, err := repo.GetAllFiles(globbedSubPath, true) if err != nil { - r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "Failed to get files for SubPath '%s'", gt.Spec.SubPath) - return nil, fmt.Errorf("failed to get all files for subpath '%s': %v", gt.Spec.SubPath, err) + r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "Failed to get files for SubPath '%s'", gt.GetSpec().SubPath) + return nil, fmt.Errorf("failed to get all files for subpath '%s': %v", gt.GetSpec().SubPath, err) } else if len(files) == 0 { - r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "No files for SubPath '%s'", gt.Spec.SubPath) - return nil, fmt.Errorf("no files for subpath '%s'", gt.Spec.SubPath) + r.recorder.Eventf(gt, apiv1.EventTypeWarning, "CheckoutFailed", "No files for SubPath '%s'", gt.GetSpec().SubPath) + return nil, fmt.Errorf("no files for subpath '%s'", gt.GetSpec().SubPath) } r.log.V(1).Info("Loaded files from repository", "file count", len(files)) @@ -243,8 +269,14 @@ func (r *ReconcileGitTrack) getFiles(gt *farosv1alpha1.GitTrack) (map[string]*gi } // fetchInstance attempts to fetch the GitTrack resource by the name in the given Request -func (r *ReconcileGitTrack) fetchInstance(req reconcile.Request) (*farosv1alpha1.GitTrack, error) { - instance := &farosv1alpha1.GitTrack{} +func (r *ReconcileGitTrack) fetchInstance(req reconcile.Request) (farosv1alpha1.GitTrackInterface, error) { + var instance farosv1alpha1.GitTrackInterface + if req.Namespace != "" { + instance = &farosv1alpha1.GitTrack{} + } else { + instance = &farosv1alpha2.ClusterGitTrack{} + } + err := r.Get(context.TODO(), req.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { @@ -260,7 +292,7 @@ func (r *ReconcileGitTrack) fetchInstance(req reconcile.Request) (*farosv1alpha1 // listObjectsByName lists and filters GitTrackObjects by the `faros.pusher.com/owned-by` label, // and returns a map of names to GitTrackObject mappings -func (r *ReconcileGitTrack) listObjectsByName(owner *farosv1alpha1.GitTrack) (map[string]farosv1alpha1.GitTrackObjectInterface, error) { +func (r *ReconcileGitTrack) listObjectsByName(owner farosv1alpha1.GitTrackInterface) (map[string]farosv1alpha1.GitTrackObjectInterface, error) { result := make(map[string]farosv1alpha1.GitTrackObjectInterface) gtos := &farosv1alpha1.GitTrackObjectList{} @@ -313,7 +345,7 @@ func successResult(namespacedName string, timeToDeploy time.Duration, inSync boo return result{NamespacedName: namespacedName, TimeToDeploy: timeToDeploy, InSync: inSync} } -func (r *ReconcileGitTrack) newGitTrackObjectInterface(name string, u *unstructured.Unstructured) (farosv1alpha1.GitTrackObjectInterface, error) { +func (r *ReconcileGitTrack) newGitTrackObjectInterface(name string, u *unstructured.Unstructured, gitTrackNamespace string) (farosv1alpha1.GitTrackObjectInterface, error) { var instance farosv1alpha1.GitTrackObjectInterface _, namespaced, err := utils.GetAPIResource(r.restMapper, u.GetObjectKind().GroupVersionKind()) if err != nil { @@ -350,15 +382,15 @@ func objectName(u *unstructured.Unstructured) string { } // handleObject either creates or updates a GitTrackObject -func (r *ReconcileGitTrack) handleObject(u *unstructured.Unstructured, owner *farosv1alpha1.GitTrack) result { +func (r *ReconcileGitTrack) handleObject(u *unstructured.Unstructured, owner farosv1alpha1.GitTrackInterface) result { name := objectName(u) - gto, err := r.newGitTrackObjectInterface(name, u) + gto, err := r.newGitTrackObjectInterface(name, u, owner.GetNamespace()) if err != nil { namespacedName := strings.TrimLeft(fmt.Sprintf("%s/%s", u.GetNamespace(), name), "/") return errorResult(namespacedName, err) } - ignored, reason, err := r.ignoreObject(u) + ignored, reason, err := r.ignoreObject(u, owner.GetNamespace()) if err != nil { return errorResult(gto.GetNamespacedName(), err) } @@ -367,7 +399,7 @@ func (r *ReconcileGitTrack) handleObject(u *unstructured.Unstructured, owner *fa } r.mutex.RLock() - timeToDeploy := time.Now().Sub(r.lastUpdateTimes[owner.Spec.Repository]) + timeToDeploy := time.Now().Sub(r.lastUpdateTimes[owner.GetSpec().Repository]) r.mutex.RUnlock() if err = controllerutil.SetControllerReference(owner, gto, r.scheme); err != nil { @@ -410,7 +442,7 @@ func childInSync(child farosv1alpha1.GitTrackObjectInterface) bool { return false } -func (r *ReconcileGitTrack) createChild(name string, timeToDeploy time.Duration, owner *farosv1alpha1.GitTrack, foundGTO, childGTO farosv1alpha1.GitTrackObjectInterface) result { +func (r *ReconcileGitTrack) createChild(name string, timeToDeploy time.Duration, owner farosv1alpha1.GitTrackInterface, foundGTO, childGTO farosv1alpha1.GitTrackObjectInterface) result { r.recorder.Eventf(owner, apiv1.EventTypeNormal, "CreateStarted", "Creating child '%s'", name) if err := r.applier.Apply(context.TODO(), &farosclient.ApplyOptions{}, childGTO); err != nil { r.recorder.Eventf(owner, apiv1.EventTypeWarning, "CreateFailed", "Failed to create child '%s'", name) @@ -471,14 +503,14 @@ func objectsFrom(files map[string]*gitstore.File) ([]*unstructured.Unstructured, // checkOwner checks the owner reference of an object from the API to see if it // is owned by the current GitTrack. -func checkOwner(owner *farosv1alpha1.GitTrack, child farosv1alpha1.GitTrackObjectInterface, s *runtime.Scheme) error { +func checkOwner(owner farosv1alpha1.GitTrackInterface, child farosv1alpha1.GitTrackObjectInterface, s *runtime.Scheme) error { gvk, err := apiutil.GVKForObject(owner, s) if err != nil { return err } for _, ref := range child.GetOwnerReferences() { - if ref.Kind == gvk.Kind && ref.UID != owner.UID { + if ref.Kind == gvk.Kind && ref.UID != owner.GetUID() { return fmt.Errorf("child object is owned by '%s'", ref.Name) } } @@ -486,16 +518,27 @@ func checkOwner(owner *farosv1alpha1.GitTrack, child farosv1alpha1.GitTrackObjec } // ignoreObject checks whether the unstructured object should be ignored -func (r *ReconcileGitTrack) ignoreObject(u *unstructured.Unstructured) (bool, string, error) { +func (r *ReconcileGitTrack) ignoreObject(u *unstructured.Unstructured, gitTrackNamespace string) (bool, string, error) { gvr, namespaced, err := utils.GetAPIResource(r.restMapper, u.GetObjectKind().GroupVersionKind()) if err != nil { return false, "", err } // Ignore namespaced objects not in the namespace managed by the controller - if namespaced && farosflags.Namespace != "" && farosflags.Namespace != u.GetNamespace() { - r.log.V(1).Info("Object not in namespace", "object namespace", u.GetNamespace(), "managed namespace", farosflags.Namespace) - return true, fmt.Sprintf("namespace `%s` is not managed by this Faros", u.GetNamespace()), nil + if namespaced { + if !farosflags.AllowCrossNamespaceOwnership && gitTrackNamespace != "" && gitTrackNamespace != u.GetNamespace() { + r.log.V(1).Info("Object not in namespace", "object namespace", u.GetNamespace(), "gittrack namespace", gitTrackNamespace) + return true, fmt.Sprintf("namespace `%s` is not managed by this Faros as GitTrack is in namespace `%s`", u.GetNamespace(), gitTrackNamespace), nil + } + if farosflags.Namespace != "" && farosflags.Namespace != u.GetNamespace() { + r.log.V(1).Info("Object not in namespace", "object namespace", u.GetNamespace(), "managed namespace", farosflags.Namespace) + return true, fmt.Sprintf("namespace `%s` is not managed by this Faros as --namespace is set to `%s`", u.GetNamespace(), farosflags.Namespace), nil + } + } else { + if !farosflags.AllowCrossNamespaceOwnership && gitTrackNamespace != "" { + r.log.V(1).Info("Object cluster-scoped", "object namespace", u.GetNamespace(), "gittrack namespace", gitTrackNamespace) + return true, fmt.Sprintf("resource is cluster-scoped but this GitTrack is in namespace `%s`", gitTrackNamespace), nil + } } // Ignore GVKs in the ignoredGVKs set if _, ok := r.ignoredGVRs[gvr]; ok { @@ -509,6 +552,7 @@ func (r *ReconcileGitTrack) ignoreObject(u *unstructured.Unstructured) (bool, st // and what is in the GitTrack.Spec // Automatically generate RBAC rules to allow the Controller to read and write Deployments // +kubebuilder:rbac:groups=faros.pusher.com,resources=gittracks,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=faros.pusher.com,resources=clustergittracks,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=faros.pusher.com,resources=gittrackobjects,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=faros.pusher.com,resources=clustergittrackobjects,verbs=get;list;watch;create;update;patch;delete func (r *ReconcileGitTrack) Reconcile(request reconcile.Request) (reconcile.Result, error) { @@ -548,7 +592,7 @@ func (r *ReconcileGitTrack) Reconcile(request reconcile.Request) (reconcile.Resu }() // Set the repository for metrics - mOpts.repository = instance.Spec.Repository + mOpts.repository = instance.GetSpec().Repository // Get a map of the files that are in the Spec files, err := reconciler.getFiles(instance) @@ -559,7 +603,7 @@ func (r *ReconcileGitTrack) Reconcile(request reconcile.Request) (reconcile.Resu } // Git successful, set condition sOpts.gitReason = gittrackutils.GitFetchSuccess - reconciler.recorder.Eventf(instance, apiv1.EventTypeNormal, "CheckoutSuccessful", "Successfully checked out '%s' at '%s'", instance.Spec.Repository, instance.Spec.Reference) + reconciler.recorder.Eventf(instance, apiv1.EventTypeNormal, "CheckoutSuccessful", "Successfully checked out '%s' at '%s'", instance.GetSpec().Repository, instance.GetSpec().Reference) // Attempt to parse k8s objects from files objects, fileErrors := objectsFrom(files) diff --git a/pkg/controller/gittrack/gittrack_controller_test.go b/pkg/controller/gittrack/gittrack_controller_test.go index c377e9ec9..94318184a 100644 --- a/pkg/controller/gittrack/gittrack_controller_test.go +++ b/pkg/controller/gittrack/gittrack_controller_test.go @@ -28,6 +28,7 @@ import ( "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" farosv1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + farosv1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" "github.com/pusher/faros/pkg/controller/gittrack/metrics" gittrackutils "github.com/pusher/faros/pkg/controller/gittrack/utils" farosflags "github.com/pusher/faros/pkg/flags" @@ -48,1075 +49,1414 @@ import ( var c client.Client var mgr manager.Manager -var instance *farosv1alpha1.GitTrack var requests chan reconcile.Request var stop chan struct{} var r reconcile.Reconciler -var key = types.NamespacedName{Name: "example", Namespace: "default"} -var expectedRequest = reconcile.Request{NamespacedName: key} - const timeout = time.Second * 5 const filePathRegexp = "^[a-zA-Z0-9/\\-\\.]*\\.(?:yaml|yml|json)$" const doesNotExistPath = "does-not-exist" const repeatedReference = "448b39a21d285fcb5aa4b718b27a3e13ffc649b3" var _ = Describe("GitTrack Suite", func() { - var createInstance = func(gt *farosv1alpha1.GitTrack, ref string) { - gt.Spec.Reference = ref - err := c.Create(context.TODO(), gt) - Expect(err).NotTo(HaveOccurred()) - } - - var waitForInstanceCreated = func(key types.NamespacedName) { - request := reconcile.Request{NamespacedName: key} - // wait for reconcile for creating the GitTrack resource - Eventually(requests, timeout).Should(Receive(Equal(request))) - // wait for reconcile for updating the GitTrack resource's status - Eventually(requests, timeout).Should(Receive(Equal(request))) - obj := &farosv1alpha1.GitTrack{} - Eventually(func() error { - err := c.Get(context.TODO(), key, obj) - if err != nil { - return err - } - if len(obj.Status.Conditions) == 0 { - return fmt.Errorf("Status not updated") - } - return nil - }, timeout).Should(Succeed()) - } - var reasonFilter = func(reason string) func(v1.Event) bool { return func(e v1.Event) bool { return e.Reason == reason } } - BeforeEach(func() { - // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a - // channel when it is finished. - var err error - cfg.RateLimiter = flowcontrol.NewFakeAlwaysRateLimiter() - mgr, err = manager.New(cfg, manager.Options{ - Namespace: farosflags.Namespace, - MetricsBindAddress: "0", // Disable serving metrics while testing - }) - Expect(err).NotTo(HaveOccurred()) - c = mgr.GetClient() - - var recFn reconcile.Reconciler - r = newReconciler(mgr) - recFn, requests = SetupTestReconcile(r) - Expect(add(mgr, recFn)).NotTo(HaveOccurred()) - stop = StartTestManager(mgr) - instance = &farosv1alpha1.GitTrack{ - ObjectMeta: metav1.ObjectMeta{ - Name: "example", - Namespace: "default", - }, - Spec: farosv1alpha1.GitTrackSpec{ - Repository: repositoryURL, - }, + Context("GitTrack", func() { + var key = types.NamespacedName{Name: "example", Namespace: "default"} + var expectedRequest = reconcile.Request{NamespacedName: key} + var instance *farosv1alpha1.GitTrack + + var createInstance = func(gt *farosv1alpha1.GitTrack, ref string) { + gt.Spec.Reference = ref + err := c.Create(context.TODO(), gt) + Expect(err).NotTo(HaveOccurred()) } - }) - AfterEach(func() { - close(stop) - testutils.DeleteAll(cfg, timeout, - &farosv1alpha1.GitTrackList{}, - &farosv1alpha1.GitTrackObjectList{}, - &farosv1alpha1.ClusterGitTrackObjectList{}, - &v1.EventList{}, - ) - }) + var waitForInstanceCreated = func(key types.NamespacedName) { + request := reconcile.Request{NamespacedName: key} + // wait for reconcile for creating the GitTrack resource + Eventually(requests, timeout).Should(Receive(Equal(request))) + // wait for reconcile for updating the GitTrack resource's status + Eventually(requests, timeout).Should(Receive(Equal(request))) + obj := &farosv1alpha1.GitTrack{} + Eventually(func() error { + err := c.Get(context.TODO(), key, obj) + if err != nil { + return err + } + if len(obj.Status.Conditions) == 0 { + return fmt.Errorf("Status not updated") + } + return nil + }, timeout).Should(Succeed()) + } - Context("When a GitTrack resource is created", func() { - Context("with a valid Spec", func() { - BeforeEach(func() { - createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") - // Wait for client cache to expire - waitForInstanceCreated(key) + BeforeEach(func() { + // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a + // channel when it is finished. + var err error + cfg.RateLimiter = flowcontrol.NewFakeAlwaysRateLimiter() + farosflags.AllowCrossNamespaceOwnership = true + mgr, err = manager.New(cfg, manager.Options{ + Namespace: farosflags.Namespace, + MetricsBindAddress: "0", // Disable serving metrics while testing }) + Expect(err).NotTo(HaveOccurred()) + c = mgr.GetClient() + + var recFn reconcile.Reconciler + r = newReconciler(mgr) + recFn, requests = SetupTestReconcile(r) + Expect(add(mgr, recFn)).NotTo(HaveOccurred()) + stop = StartTestManager(mgr) + instance = &farosv1alpha1.GitTrack{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "default", + }, + Spec: farosv1alpha1.GitTrackSpec{ + Repository: repositoryURL, + }, + } + }) - It("updates its status", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - two, zero := int64(2), int64(0) - Expect(instance.Status.ObjectsDiscovered).To(Equal(two)) - Expect(instance.Status.ObjectsApplied).To(Equal(two)) - Expect(instance.Status.ObjectsIgnored).To(Equal(zero)) - Expect(instance.Status.ObjectsInSync).To(Equal(zero)) - - deployGto := &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) - }, timeout).Should(Succeed()) - - now := metav1.NewTime(time.Now()) - deployGto.Status.Conditions = []farosv1alpha1.GitTrackObjectCondition{ - { - Type: farosv1alpha1.ObjectInSyncType, - Status: v1.ConditionTrue, - LastTransitionTime: now, - LastUpdateTime: now, - }, - } - Expect(c.Update(context.TODO(), deployGto)).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.ObjectsInSync).To(Equal(int64(1))) - }) + AfterEach(func() { + close(stop) + testutils.DeleteAll(cfg, timeout, + &farosv1alpha1.GitTrackList{}, + &farosv1alpha2.ClusterGitTrackList{}, + &farosv1alpha1.GitTrackObjectList{}, + &farosv1alpha1.ClusterGitTrackObjectList{}, + &v1.EventList{}, + ) + }) - It("sets the status conditions", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - conditions := instance.Status.Conditions - Expect(len(conditions)).To(Equal(4)) - parseErrorCondition := conditions[0] - gitErrorCondition := conditions[1] - gcErrorCondition := conditions[2] - upToDateCondiiton := conditions[3] - Expect(parseErrorCondition.Type).To(Equal(farosv1alpha1.FilesParsedType)) - Expect(gitErrorCondition.Type).To(Equal(farosv1alpha1.FilesFetchedType)) - Expect(gcErrorCondition.Type).To(Equal(farosv1alpha1.ChildrenGarbageCollectedType)) - Expect(upToDateCondiiton.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) - }) + Context("When a GitTrack resource is created", func() { + Context("with a valid Spec", func() { + BeforeEach(func() { + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("updates its status", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + two, zero := int64(2), int64(0) + Expect(instance.Status.ObjectsDiscovered).To(Equal(two)) + Expect(instance.Status.ObjectsApplied).To(Equal(two)) + Expect(instance.Status.ObjectsIgnored).To(Equal(zero)) + Expect(instance.Status.ObjectsInSync).To(Equal(zero)) + + deployGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + + now := metav1.NewTime(time.Now()) + deployGto.Status.Conditions = []farosv1alpha1.GitTrackObjectCondition{ + { + Type: farosv1alpha1.ObjectInSyncType, + Status: v1.ConditionTrue, + LastTransitionTime: now, + LastUpdateTime: now, + }, + } + Expect(c.Update(context.TODO(), deployGto)).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Status.ObjectsInSync).To(Equal(int64(1))) + }) + + It("sets the status conditions", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + conditions := instance.Status.Conditions + Expect(len(conditions)).To(Equal(4)) + parseErrorCondition := conditions[0] + gitErrorCondition := conditions[1] + gcErrorCondition := conditions[2] + upToDateCondiiton := conditions[3] + Expect(parseErrorCondition.Type).To(Equal(farosv1alpha1.FilesParsedType)) + Expect(gitErrorCondition.Type).To(Equal(farosv1alpha1.FilesFetchedType)) + Expect(gcErrorCondition.Type).To(Equal(farosv1alpha1.ChildrenGarbageCollectedType)) + Expect(upToDateCondiiton.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) + }) + + Context("sets the status metrics", func() { + var setsMetric = func(status string, value float64) { + It(fmt.Sprintf("sets status `%s` to %f", status, value), func() { + var gauge prometheus.Gauge + Eventually(func() error { + var err error + gauge, err = metrics.ChildStatus.GetMetricWith(map[string]string{ + "name": instance.GetName(), + "namespace": instance.GetNamespace(), + "status": status, + }) + return err + }, timeout).Should(Succeed()) + var metric dto.Metric + Expect(gauge.Write(&metric)).NotTo(HaveOccurred()) + Expect(metric.GetGauge().GetValue()).To(Equal(value)) + }) + } - Context("sets the status metrics", func() { - var setsMetric = func(status string, value float64) { - It(fmt.Sprintf("sets status `%s` to %f", status, value), func() { - var gauge prometheus.Gauge - Eventually(func() error { - var err error - gauge, err = metrics.ChildStatus.GetMetricWith(map[string]string{ - "name": instance.GetName(), - "namespace": instance.GetNamespace(), - "status": status, - }) - return err - }, timeout).Should(Succeed()) - var metric dto.Metric - Expect(gauge.Write(&metric)).NotTo(HaveOccurred()) - Expect(metric.GetGauge().GetValue()).To(Equal(value)) - }) - } + setsMetric("discovered", 2.0) + setsMetric("applied", 2.0) + setsMetric("ignored", 0.0) + setsMetric("inSync", 0.0) + }) + + It("creates GitTrackObjects", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + serviceGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) + }, timeout).Should(Succeed()) + }) + + It("sets ownerReferences for created GitTrackObjects", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + serviceGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) + }, timeout).Should(Succeed()) + Expect(len(deployGto.OwnerReferences)).To(Equal(1)) + Expect(len(serviceGto.OwnerReferences)).To(Equal(1)) + }) + + It("sets LastAppliedAnnotations for created GitTrackObjects", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + serviceGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) + }, timeout).Should(Succeed()) + Expect(deployGto.GetAnnotations()).To(HaveKey(farosclient.LastAppliedAnnotation)) + Expect(serviceGto.GetAnnotations()).To(HaveKey(farosclient.LastAppliedAnnotation)) + }) + + It("sends events about checking out configured Git repository", func() { + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + startEvents := testevents.Select(events.Items, reasonFilter("CheckoutStarted")) + successEvents := testevents.Select(events.Items, reasonFilter("CheckoutSuccessful")) + Expect(startEvents).ToNot(BeEmpty()) + Expect(successEvents).ToNot(BeEmpty()) + for _, e := range append(startEvents, successEvents...) { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) + } + }) - setsMetric("discovered", 2.0) - setsMetric("applied", 2.0) - setsMetric("ignored", 0.0) - setsMetric("inSync", 0.0) + It("sends events about creating GitTrackObjects", func() { + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + startEvents := testevents.Select(events.Items, reasonFilter("CreateStarted")) + successEvents := testevents.Select(events.Items, reasonFilter("CreateSuccessful")) + Expect(startEvents).ToNot(BeEmpty()) + Expect(successEvents).ToNot(BeEmpty()) + for _, e := range append(startEvents, successEvents...) { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) + } + }) }) - It("creates GitTrackObjects", func() { - deployGto := &farosv1alpha1.GitTrackObject{} - serviceGto := &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) - }, timeout).Should(Succeed()) + Context("with multi-document YAML", func() { + BeforeEach(func() { + createInstance(instance, "9bf412f0e893c8c1624bb1c523cfeca8243534bc") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("creates GitTrackObjects", func() { + dsGto, cmGto := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "daemonset-fluentd", Namespace: "default"}, dsGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-fluentd-config", Namespace: "default"}, cmGto) + }, timeout).Should(Succeed()) + }) }) - It("sets ownerReferences for created GitTrackObjects", func() { - deployGto := &farosv1alpha1.GitTrackObject{} - serviceGto := &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) - }, timeout).Should(Succeed()) - Expect(len(deployGto.OwnerReferences)).To(Equal(1)) - Expect(len(serviceGto.OwnerReferences)).To(Equal(1)) + Context("with a cluster scoped resource", func() { + BeforeEach(func() { + createInstance(instance, "b17c0e0f45beca3f1c1e62a7f49fecb738c60d42") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("does not create ClusterGitTrackObject", func() { + nsCGto := &farosv1alpha1.ClusterGitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "namespace-test", Namespace: ""}, nsCGto) + }, timeout).Should(Succeed()) + }) }) - It("sets LastAppliedAnnotations for created GitTrackObjects", func() { - deployGto := &farosv1alpha1.GitTrackObject{} - serviceGto := &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) - }, timeout).Should(Succeed()) - Expect(deployGto.GetAnnotations()).To(HaveKey(farosclient.LastAppliedAnnotation)) - Expect(serviceGto.GetAnnotations()).To(HaveKey(farosclient.LastAppliedAnnotation)) - }) + Context("with an invalid Reference", func() { + BeforeEach(func() { + createInstance(instance, doesNotExistPath) + // Wait for client cache to expire + waitForInstanceCreated(key) + }) - It("sends events about checking out configured Git repository", func() { - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - startEvents := testevents.Select(events.Items, reasonFilter("CheckoutStarted")) - successEvents := testevents.Select(events.Items, reasonFilter("CheckoutSuccessful")) - Expect(startEvents).ToNot(BeEmpty()) - Expect(successEvents).ToNot(BeEmpty()) - for _, e := range append(startEvents, successEvents...) { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) - } + It("updates the FilesFetched condition", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + // TODO: don't rely on ordering + c := instance.Status.Conditions[1] + Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) + Expect(c.Status).To(Equal(v1.ConditionFalse)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) + Expect(c.Message).To(Equal("failed to checkout 'does-not-exist': unable to parse ref does-not-exist: reference not found")) + }) + + It("sends a CheckoutFailed event", func() { + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + failedEvents := testevents.Select(events.Items, reasonFilter("CheckoutFailed")) + Expect(failedEvents).ToNot(BeEmpty()) + for _, e := range failedEvents { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(string(v1.EventTypeWarning))) + } + }) }) - It("sends events about creating GitTrackObjects", func() { - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - startEvents := testevents.Select(events.Items, reasonFilter("CreateStarted")) - successEvents := testevents.Select(events.Items, reasonFilter("CreateSuccessful")) - Expect(startEvents).ToNot(BeEmpty()) - Expect(successEvents).ToNot(BeEmpty()) - for _, e := range append(startEvents, successEvents...) { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) - } + Context("with an invalid SubPath", func() { + BeforeEach(func() { + instance.Spec.SubPath = doesNotExistPath + createInstance(instance, "master") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("updates the FilesFetched condition", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + // TODO: don't rely on ordering + c := instance.Status.Conditions[1] + Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) + Expect(c.Status).To(Equal(v1.ConditionFalse)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) + Expect(c.Message).To(Equal("no files for subpath 'does-not-exist'")) + }) + + It("sends a CheckoutFailed event", func() { + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + failedEvents := testevents.Select(events.Items, reasonFilter("CheckoutFailed")) + Expect(failedEvents).ToNot(BeEmpty()) + for _, e := range failedEvents { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(v1.EventTypeWarning)) + } + }) }) - }) - Context("with multi-document YAML", func() { - BeforeEach(func() { - createInstance(instance, "9bf412f0e893c8c1624bb1c523cfeca8243534bc") - // Wait for client cache to expire - waitForInstanceCreated(key) + Context("with files from an unmanaged namespace", func() { + BeforeEach(func() { + instance.Spec.SubPath = "foo" + createInstance(instance, "4c31dbdd7103dc209c8bb21b75d78b3efafadc31") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("ignores the files", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + + // TODO: don't rely on ordering + c := instance.Status.Conditions[3] + Expect(c.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) + Expect(c.Status).To(Equal(v1.ConditionTrue)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ChildrenUpdateSuccess))) + + Expect(instance.Status.ObjectsIgnored).To(Equal(int64(2))) + }) + + It("adds a message to the ignoredFiles status", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("foo/deployment-nginx", "namespace `foo` is not managed by this Faros as --namespace is set to `default`")) + Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("foo/service-nginx", "namespace `foo` is not managed by this Faros as --namespace is set to `default`")) + }) + + It("includes the ignored files in ignoredObjects count", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Status.IgnoredFiles).To(HaveLen(int(instance.Status.ObjectsIgnored))) + }) }) - It("creates GitTrackObjects", func() { - dsGto, cmGto := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "daemonset-fluentd", Namespace: "default"}, dsGto) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-fluentd-config", Namespace: "default"}, cmGto) - }, timeout).Should(Succeed()) + Context("with a child owned by another controller", func() { + truth := true + var existingChild *farosv1alpha1.GitTrackObject + BeforeEach(func() { + existingChild = &farosv1alpha1.GitTrackObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deployment-nginx", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "faros.pusher.com/v1alpha1", + Kind: "GitTrack", + Name: doesNotExistPath, + UID: "12345", + Controller: &truth, + BlockOwnerDeletion: &truth, + }, + }, + }, + Spec: farosv1alpha1.GitTrackObjectSpec{ + Name: "nginx", + Kind: "Deployment", + Data: []byte("kind: Deployment"), + }, + } + err := c.Create(context.TODO(), existingChild) + Expect(err).ToNot(HaveOccurred()) + + createInstance(instance, "4c31dbdd7103dc209c8bb21b75d78b3efafadc31") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("should not overwrite the existing child", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + + o := deployGto.ObjectMeta + Expect(o.OwnerReferences).To(Equal(existingChild.ObjectMeta.OwnerReferences)) + Expect(o.Name).To(Equal(existingChild.ObjectMeta.Name)) + Expect(o.Namespace).To(Equal(existingChild.ObjectMeta.Namespace)) + + Expect(deployGto.Spec).To(Equal(existingChild.Spec)) + }) + + It("should ignore the GitTrackObject", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + + // TODO: don't rely on ordering + c := instance.Status.Conditions[3] + Expect(c.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) + Expect(c.Status).To(Equal(v1.ConditionTrue)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ChildrenUpdateSuccess))) + + Expect(instance.Status.ObjectsIgnored).To(Equal(int64(3))) + }) }) - }) - Context("with a cluster scoped resource", func() { - BeforeEach(func() { - createInstance(instance, "b17c0e0f45beca3f1c1e62a7f49fecb738c60d42") - // Wait for client cache to expire - waitForInstanceCreated(key) + Context("with a child resource that has a name that contains `:`", func() { + BeforeEach(func() { + createInstance(instance, "241786090da55894dca4e91e3f5023c024d3d9a8") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("replaces `:` with `-`", func() { + clusterRoleGto := &farosv1alpha1.ClusterGitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "clusterrole-test-read-ns-pods-svcs"}, clusterRoleGto) + }, timeout).Should(Succeed()) + Expect(clusterRoleGto.Name).To(Equal("clusterrole-test-read-ns-pods-svcs")) + }) }) - It("creates ClusterGitTrackObject", func() { - nsCGto := &farosv1alpha1.ClusterGitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "namespace-test", Namespace: ""}, nsCGto) - }, timeout).Should(Succeed()) + Context("in a different namespace", func() { + var ns *v1.Namespace + BeforeEach(func() { + ns = &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "not-default", + }, + } + Expect(c.Create(context.TODO(), ns)).NotTo(HaveOccurred()) + instance.Namespace = "not-default" + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + }) + + AfterEach(func() { + Expect(c.Delete(context.TODO(), ns)).NotTo(HaveOccurred()) + }) + + It("should not reconcile it", func() { + Eventually(requests, timeout).ShouldNot(Receive()) + }) }) }) - Context("with an invalid Reference", func() { - BeforeEach(func() { - createInstance(instance, doesNotExistPath) - // Wait for client cache to expire - waitForInstanceCreated(key) + Context("When a GitTrack resource is updated", func() { + Context("and resources are added to the repository", func() { + BeforeEach(func() { + createInstance(instance, "28928ccaeb314b96293e18cc8889997f0f46b79b") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("creates the new resources", func() { + before, after := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Spec.Reference).To(Equal("28928ccaeb314b96293e18cc8889997f0f46b79b")) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "ingress-example", Namespace: "default"}, before) + }, timeout).ShouldNot(Succeed()) + + instance.Spec.Reference = "09d24c51c191b4caacd35cda23bd44c86f16edc6" + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "ingress-example", Namespace: "default"}, after) + }, timeout).Should(Succeed()) + }) }) - It("updates the FilesFetched condition", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - // TODO: don't rely on ordering - c := instance.Status.Conditions[1] - Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) - Expect(c.Status).To(Equal(v1.ConditionFalse)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) - Expect(c.Message).To(Equal("failed to checkout 'does-not-exist': unable to parse ref does-not-exist: reference not found")) + Context("and resources are removed from the repository", func() { + BeforeEach(func() { + createInstance(instance, "4532b487a5aaf651839f5401371556aa16732a6e") + // Wait for client cache to expire + waitForInstanceCreated(key) + + // Check the instance created + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Spec.Reference).To(Equal("4532b487a5aaf651839f5401371556aa16732a6e")) + + // Check the configmap to be deleted was created + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-deleted-config", Namespace: "default"}, &farosv1alpha1.GitTrackObject{}) + }, timeout).Should(Succeed()) + + // Update the repository + instance.Spec.Reference = "28928ccaeb314b96293e18cc8889997f0f46b79b" + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + + // Wait for cache to sync + waitForInstanceCreated(key) + }) + + It("deletes the removed resources", func() { + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-deleted-config", Namespace: "default"}, &farosv1alpha1.GitTrackObject{}) + }, timeout).ShouldNot(Succeed()) + }) + + It("doesn't delete any other resources", func() { + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-deleted-config", Namespace: "default"}, &farosv1alpha1.GitTrackObject{}) + }, timeout).ShouldNot(Succeed()) + + gtos := &farosv1alpha1.GitTrackObjectList{} + err := c.List(context.TODO(), gtos, client.InNamespace(instance.Namespace)) + Expect(err).ToNot(HaveOccurred()) + Expect(len(gtos.Items)).To(Equal(2)) + }) }) - It("sends a CheckoutFailed event", func() { - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - failedEvents := testevents.Select(events.Items, reasonFilter("CheckoutFailed")) - Expect(failedEvents).ToNot(BeEmpty()) - for _, e := range failedEvents { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Type).To(Equal(string(v1.EventTypeWarning))) - } + Context("and resources in the repository are updated", func() { + BeforeEach(func() { + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("updates the updated resources", func() { + before, after := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Spec.Reference).To(Equal("a14443638218c782b84cae56a14f1090ee9e5c9c")) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, before) + }, timeout).Should(Succeed()) + + instance.Spec.Reference = repeatedReference + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + + Eventually(func() error { + err = c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, after) + if err != nil { + return nil + } + if reflect.DeepEqual(after.Spec, before.Spec) { + return fmt.Errorf("deployment not updated yet") + } + return nil + }, timeout).Should(Succeed()) + Expect(after.Spec).ToNot(Equal(before.Spec)) + }) + + It("doesn't modify any other resources", func() { + before, after := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Spec.Reference).To(Equal("a14443638218c782b84cae56a14f1090ee9e5c9c")) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, before) + }, timeout).Should(Succeed()) + + instance.Spec.Reference = repeatedReference + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, after) + }, timeout).Should(Succeed()) + Expect(after.Spec).To(Equal(before.Spec)) + }) + + It("sends events about updating resources", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + instance.Spec.Reference = repeatedReference + Expect(c.Update(context.TODO(), instance)).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { + events := &v1.EventList{} + err := c.List(context.TODO(), events) + if err != nil { + return err + } + if testevents.None(events.Items, reasonFilter("UpdateSuccessful")) { + return fmt.Errorf("events hasn't been sent yet") + } + return nil + }, timeout*2).Should(Succeed()) + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + successEvents := testevents.Select(events.Items, reasonFilter("UpdateSuccessful")) + failedEvents := testevents.Select(events.Items, reasonFilter("UpdateFailed")) + Expect(successEvents).ToNot(BeEmpty()) + Expect(failedEvents).To(BeEmpty()) + for _, e := range successEvents { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) + Expect(e.Reason).To(Equal("UpdateSuccessful")) + } + }) + + It("updates the time to deploy metric", func() { + // Reset the metric before testing + metrics.TimeToDeploy.Reset() + + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Spec.Reference).To(Equal("a14443638218c782b84cae56a14f1090ee9e5c9c")) + + // Update the reference + instance.Spec.Reference = repeatedReference + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + + Eventually(func() error { + labels := map[string]string{ + "name": instance.GetName(), + "namespace": instance.GetNamespace(), + "repository": instance.Spec.Repository, + } + histObserver := metrics.TimeToDeploy.With(labels) + hist := histObserver.(prometheus.Histogram) + var timeToDeploy dto.Metric + hist.Write(&timeToDeploy) + if timeToDeploy.GetHistogram().GetSampleCount() != uint64(4) { + return fmt.Errorf("metrics not updated") + } + return nil + }, timeout).Should(Succeed()) + }) }) - }) - Context("with an invalid SubPath", func() { - BeforeEach(func() { - instance.Spec.SubPath = doesNotExistPath - createInstance(instance, "master") - // Wait for client cache to expire - waitForInstanceCreated(key) + Context("and the subPath has changed", func() { }) - It("updates the FilesFetched condition", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - // TODO: don't rely on ordering - c := instance.Status.Conditions[1] - Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) - Expect(c.Status).To(Equal(v1.ConditionFalse)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) - Expect(c.Message).To(Equal("no files for subpath 'does-not-exist'")) + Context("and the reference is invalid", func() { + BeforeEach(func() { + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("does not update any of the resources", func() { + deployBefore, deployAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + serviceBefore, serviceAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployBefore) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceBefore) + }, timeout).Should(Succeed()) + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + + instance.Spec.Reference = doesNotExistPath + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployAfter) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceAfter) + }, timeout).Should(Succeed()) + + Expect(deployBefore).To(Equal(deployAfter)) + Expect(serviceBefore).To(Equal(serviceAfter)) + }) + + It("updates the FilesFetched condition", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + instance.Spec.Reference = doesNotExistPath + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { + err = c.Get(context.TODO(), key, instance) + if err != nil { + return err + } + c := instance.Status.Conditions[1] + if c.Status != v1.ConditionFalse { + return fmt.Errorf("condition hasn't updated yet") + } + return nil + }, timeout).Should(Succeed()) + // TODO: don't rely on ordering + c := instance.Status.Conditions[1] + Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) + Expect(c.Message).To(Equal("failed to checkout 'does-not-exist': unable to parse ref does-not-exist: reference not found")) + }) + + It("sends a CheckoutFailed event", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + instance.Spec.Reference = doesNotExistPath + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { + events := &v1.EventList{} + err := c.List(context.TODO(), events) + if err != nil { + return err + } + if testevents.None(events.Items, reasonFilter("CheckoutFailed")) { + return fmt.Errorf("events hasn't been sent yet") + } + return nil + }, timeout).Should(Succeed()) + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + failedEvents := testevents.Select(events.Items, reasonFilter("CheckoutFailed")) + Expect(failedEvents).ToNot(BeEmpty()) + for _, e := range failedEvents { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(v1.EventTypeWarning)) + } + }) }) - It("sends a CheckoutFailed event", func() { - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - failedEvents := testevents.Select(events.Items, reasonFilter("CheckoutFailed")) - Expect(failedEvents).ToNot(BeEmpty()) - for _, e := range failedEvents { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Type).To(Equal(v1.EventTypeWarning)) - } + Context("and the subPath is invalid", func() { + BeforeEach(func() { + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("does not update any of the resources", func() { + deployBefore, deployAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + serviceBefore, serviceAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployBefore) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceBefore) + }, timeout).Should(Succeed()) + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + + instance.Spec.SubPath = doesNotExistPath + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployAfter) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceAfter) + }, timeout).Should(Succeed()) + + Expect(deployBefore).To(Equal(deployAfter)) + Expect(serviceBefore).To(Equal(serviceAfter)) + }) + + It("updates the FilesFetched condition", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + instance.Spec.SubPath = doesNotExistPath + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { + err = c.Get(context.TODO(), key, instance) + if err != nil { + return err + } + c := instance.Status.Conditions[1] + if c.Status != v1.ConditionFalse { + return fmt.Errorf("condition hasn't updated yet") + } + return nil + }, timeout).Should(Succeed()) + // TODO: don't rely on ordering + c := instance.Status.Conditions[1] + Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) + Expect(c.Message).To(Equal("no files for subpath 'does-not-exist'")) + }) + + It("sends a CheckoutFailed event", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + instance.Spec.SubPath = doesNotExistPath + err := c.Update(context.TODO(), instance) + Expect(err).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + filter := func(e v1.Event) bool { + return e.Reason == "CheckoutFailed" && e.Message == "No files for SubPath 'does-not-exist'" + } + Eventually(func() error { + events := &v1.EventList{} + err := c.List(context.TODO(), events) + if err != nil { + return err + } + if testevents.None(events.Items, filter) { + return fmt.Errorf("events hasn't been sent yet") + } + return nil + }, timeout).Should(Succeed()) + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + failedEvents := testevents.Select(events.Items, filter) + Expect(failedEvents).ToNot(BeEmpty()) + for _, e := range failedEvents { + Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Message).To(Equal("No files for SubPath 'does-not-exist'")) + Expect(e.Type).To(Equal(string(v1.EventTypeWarning))) + } + }) }) }) - Context("with files from an unmanaged namespace", func() { - BeforeEach(func() { - instance.Spec.SubPath = "foo" - createInstance(instance, "4c31dbdd7103dc209c8bb21b75d78b3efafadc31") - // Wait for client cache to expire - waitForInstanceCreated(key) - }) - - It("ignores the files", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Context("When a GitTrack resource is deleted", func() { + }) - // TODO: don't rely on ordering - c := instance.Status.Conditions[3] - Expect(c.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) - Expect(c.Status).To(Equal(v1.ConditionTrue)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ChildrenUpdateSuccess))) + Context("When a GitTrack has a DeployKey, the Reconciler should", func() { + var reconciler *ReconcileGitTrack + var s *v1.Secret + var keyRef farosv1alpha1.GitTrackDeployKey + var expectedKey []byte - Expect(instance.Status.ObjectsIgnored).To(Equal(int64(2))) - }) + keysMustBeSetErr := errors.New("if using a deploy key, both SecretName and Key must be set") + secretNotFoundErr := errors.New("failed to look up secret nonExistSecret: Secret \"nonExistSecret\" not found") - It("adds a message to the ignoredFiles status", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("foo/deployment-nginx", "namespace `foo` is not managed by this Faros")) - Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("foo/service-nginx", "namespace `foo` is not managed by this Faros")) - }) + BeforeEach(func() { + var ok bool + reconciler, ok = r.(*ReconcileGitTrack) + Expect(ok).To(BeTrue()) - It("includes the ignored files in ignoredObjects count", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.IgnoredFiles).To(HaveLen(int(instance.Status.ObjectsIgnored))) - }) - }) + keyRef = farosv1alpha1.GitTrackDeployKey{ + SecretName: "foosecret", + Key: "privatekey", + } - Context("with a child owned by another controller", func() { - truth := true - var existingChild *farosv1alpha1.GitTrackObject - BeforeEach(func() { - existingChild = &farosv1alpha1.GitTrackObject{ + expectedKey = []byte("PrivateKey") + s = &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "deployment-nginx", + Name: "foosecret", Namespace: "default", - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "faros.pusher.com/v1alpha1", - Kind: "GitTrack", - Name: doesNotExistPath, - UID: "12345", - Controller: &truth, - BlockOwnerDeletion: &truth, - }, - }, }, - Spec: farosv1alpha1.GitTrackObjectSpec{ - Name: "nginx", - Kind: "Deployment", - Data: []byte("kind: Deployment"), + Data: map[string][]byte{ + "privatekey": expectedKey, }, } - err := c.Create(context.TODO(), existingChild) - Expect(err).ToNot(HaveOccurred()) - - createInstance(instance, "4c31dbdd7103dc209c8bb21b75d78b3efafadc31") - // Wait for client cache to expire - waitForInstanceCreated(key) + Expect(c.Create(context.TODO(), s)).NotTo(HaveOccurred()) }) - It("should not overwrite the existing child", func() { - deployGto := &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) - }, timeout).Should(Succeed()) - - o := deployGto.ObjectMeta - Expect(o.OwnerReferences).To(Equal(existingChild.ObjectMeta.OwnerReferences)) - Expect(o.Name).To(Equal(existingChild.ObjectMeta.Name)) - Expect(o.Namespace).To(Equal(existingChild.ObjectMeta.Namespace)) - - Expect(deployGto.Spec).To(Equal(existingChild.Spec)) + AfterEach(func() { + c.Delete(context.TODO(), s) }) - It("should ignore the GitTrackObject", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - - // TODO: don't rely on ordering - c := instance.Status.Conditions[3] - Expect(c.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) - Expect(c.Status).To(Equal(v1.ConditionTrue)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ChildrenUpdateSuccess))) - - Expect(instance.Status.ObjectsIgnored).To(Equal(int64(3))) + It("do nothing if the secret name and key are empty", func() { + key, err := reconciler.fetchGitCredentials("default", farosv1alpha1.GitTrackDeployKey{}) + Expect(err).NotTo(HaveOccurred()) + Expect(key).To(BeNil()) }) - }) - Context("with a child resource that has a name that contains `:`", func() { - BeforeEach(func() { - createInstance(instance, "241786090da55894dca4e91e3f5023c024d3d9a8") - // Wait for client cache to expire - waitForInstanceCreated(key) + It("get the key from the secret", func() { + key, err := reconciler.fetchGitCredentials("default", keyRef) + Expect(err).NotTo(HaveOccurred()) + Expect(key.secret).To(Equal(expectedKey)) }) - It("replaces `:` with `-`", func() { - clusterRoleGto := &farosv1alpha1.ClusterGitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "clusterrole-test-read-ns-pods-svcs"}, clusterRoleGto) - }, timeout).Should(Succeed()) - Expect(clusterRoleGto.Name).To(Equal("clusterrole-test-read-ns-pods-svcs")) + It("return an error if the secret doesn't exist", func() { + keyRef.SecretName = "nonExistSecret" + key, err := reconciler.fetchGitCredentials("default", keyRef) + Expect(err).To(Equal(secretNotFoundErr)) + Expect(key).To(BeNil()) }) - }) - Context("in a different namespace", func() { - var ns *v1.Namespace - BeforeEach(func() { - ns = &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "not-default", - }, - } - Expect(c.Create(context.TODO(), ns)).NotTo(HaveOccurred()) - instance.Namespace = "not-default" - createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + It("return an error if the secret name isnt set, but the key is", func() { + keyRef.SecretName = "" + key, err := reconciler.fetchGitCredentials("default", keyRef) + Expect(err).To(Equal(keysMustBeSetErr)) + Expect(key).To(BeNil()) }) - AfterEach(func() { - Expect(c.Delete(context.TODO(), ns)).NotTo(HaveOccurred()) + It("return an error if the key isnt set, but the secret name is", func() { + keyRef.Key = "" + key, err := reconciler.fetchGitCredentials("default", keyRef) + Expect(err).To(Equal(keysMustBeSetErr)) + Expect(key).To(BeNil()) }) + }) - It("should not reconcile it", func() { - Eventually(requests, timeout).ShouldNot(Receive()) - }) + Context("When getting files from a repository", func() { + /* + foo + ├── bar + │   ├── non-yaml-file.txt + │   └── service.yaml + ├── deployment.yaml + └── namespace.yaml + */ + + getsFilesFromRepo("foo", 3) + getsFilesFromRepo("foo/", 3) + getsFilesFromRepo("/foo/", 3) + getsFilesFromRepo("foo/bar", 1) + getsFilesFromRepo("foobar", 2) + getsFilesFromRepo("foobar/", 2) }) - }) - Context("When a GitTrack resource is updated", func() { - Context("and resources are added to the repository", func() { + Context(fmt.Sprintf("with invalid files"), func() { BeforeEach(func() { - createInstance(instance, "28928ccaeb314b96293e18cc8889997f0f46b79b") - // Wait for client cache to expire + createInstance(instance, "936b7ee3df1dbd61b1fc691b742fa5d5d3c0dced") waitForInstanceCreated(key) }) - It("creates the new resources", func() { - before, after := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Spec.Reference).To(Equal("28928ccaeb314b96293e18cc8889997f0f46b79b")) - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "ingress-example", Namespace: "default"}, before) - }, timeout).ShouldNot(Succeed()) - - instance.Spec.Reference = "09d24c51c191b4caacd35cda23bd44c86f16edc6" - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + It("adds a message to the ignoredFiles status", func() { Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "ingress-example", Namespace: "default"}, after) - }, timeout).Should(Succeed()) + Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("invalid_file.yaml", "unable to parse 'invalid_file.yaml': unable to unmarshal JSON: Object 'Kind' is missing in '{\"I\":\"a;m an \\\"invalid Kubernetes manifest.)\"}'\n")) }) - }) - Context("and resources are removed from the repository", func() { - BeforeEach(func() { - createInstance(instance, "4532b487a5aaf651839f5401371556aa16732a6e") - // Wait for client cache to expire - waitForInstanceCreated(key) - - // Check the instance created + It("includes the invalid file in ignoredObjects count", func() { Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Spec.Reference).To(Equal("4532b487a5aaf651839f5401371556aa16732a6e")) - - // Check the configmap to be deleted was created - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-deleted-config", Namespace: "default"}, &farosv1alpha1.GitTrackObject{}) - }, timeout).Should(Succeed()) - - // Update the repository - instance.Spec.Reference = "28928ccaeb314b96293e18cc8889997f0f46b79b" - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - - // Wait for cache to sync - waitForInstanceCreated(key) - }) - - It("deletes the removed resources", func() { - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-deleted-config", Namespace: "default"}, &farosv1alpha1.GitTrackObject{}) - }, timeout).ShouldNot(Succeed()) - }) - - It("doesn't delete any other resources", func() { - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "configmap-deleted-config", Namespace: "default"}, &farosv1alpha1.GitTrackObject{}) - }, timeout).ShouldNot(Succeed()) - - gtos := &farosv1alpha1.GitTrackObjectList{} - err := c.List(context.TODO(), gtos, client.InNamespace(instance.Namespace)) - Expect(err).ToNot(HaveOccurred()) - Expect(len(gtos.Items)).To(Equal(2)) + Expect(instance.Status.IgnoredFiles).To(HaveLen(int(instance.Status.ObjectsIgnored))) }) }) - Context("and resources in the repository are updated", func() { + Context("When a list of ignored GVRs is supplied", func() { BeforeEach(func() { + reconciler, ok := r.(*ReconcileGitTrack) + Expect(ok).To(BeTrue()) + reconciler.ignoredGVRs = make(map[schema.GroupVersionResource]interface{}) + deploymentGVR := schema.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + } + reconciler.ignoredGVRs[deploymentGVR] = nil + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") // Wait for client cache to expire waitForInstanceCreated(key) }) - It("updates the updated resources", func() { - before, after := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} + It("ignores the deployment files", func() { Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Spec.Reference).To(Equal("a14443638218c782b84cae56a14f1090ee9e5c9c")) - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, before) - }, timeout).Should(Succeed()) - - instance.Spec.Reference = repeatedReference - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - - Eventually(func() error { - err = c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, after) - if err != nil { - return nil - } - if reflect.DeepEqual(after.Spec, before.Spec) { - return fmt.Errorf("deployment not updated yet") - } - return nil - }, timeout).Should(Succeed()) - Expect(after.Spec).ToNot(Equal(before.Spec)) - }) - It("doesn't modify any other resources", func() { - before, after := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Spec.Reference).To(Equal("a14443638218c782b84cae56a14f1090ee9e5c9c")) - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, before) - }, timeout).Should(Succeed()) - - instance.Spec.Reference = repeatedReference - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + // TODO: don't rely on ordering + c := instance.Status.Conditions[3] + Expect(c.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) + Expect(c.Status).To(Equal(v1.ConditionTrue)) + Expect(c.LastUpdateTime).NotTo(BeNil()) + Expect(c.LastTransitionTime).NotTo(BeNil()) + Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) + Expect(c.Reason).To(Equal(string(gittrackutils.ChildrenUpdateSuccess))) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, after) - }, timeout).Should(Succeed()) - Expect(after.Spec).To(Equal(before.Spec)) + Expect(instance.Status.ObjectsIgnored).To(Equal(int64(1))) }) - It("sends events about updating resources", func() { + It("adds a message to the ignoredFiles status", func() { Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - instance.Spec.Reference = repeatedReference - Expect(c.Update(context.TODO(), instance)).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - Eventually(func() error { - events := &v1.EventList{} - err := c.List(context.TODO(), events) - if err != nil { - return err - } - if testevents.None(events.Items, reasonFilter("UpdateSuccessful")) { - return fmt.Errorf("events hasn't been sent yet") - } - return nil - }, timeout*2).Should(Succeed()) - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - successEvents := testevents.Select(events.Items, reasonFilter("UpdateSuccessful")) - failedEvents := testevents.Select(events.Items, reasonFilter("UpdateFailed")) - Expect(successEvents).ToNot(BeEmpty()) - Expect(failedEvents).To(BeEmpty()) - for _, e := range successEvents { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) - Expect(e.Reason).To(Equal("UpdateSuccessful")) - } + Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("default/deployment-nginx", "resource `deployments.apps/v1` ignored globally by flag")) }) - It("updates the time to deploy metric", func() { - // Reset the metric before testing - metrics.TimeToDeploy.Reset() - + It("includes the ignored files in ignoredObjects count", func() { Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Spec.Reference).To(Equal("a14443638218c782b84cae56a14f1090ee9e5c9c")) - - // Update the reference - instance.Spec.Reference = repeatedReference - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - - Eventually(func() error { - labels := map[string]string{ - "name": instance.GetName(), - "namespace": instance.GetNamespace(), - "repository": instance.Spec.Repository, - } - histObserver := metrics.TimeToDeploy.With(labels) - hist := histObserver.(prometheus.Histogram) - var timeToDeploy dto.Metric - hist.Write(&timeToDeploy) - if timeToDeploy.GetHistogram().GetSampleCount() != uint64(4) { - return fmt.Errorf("metrics not updated") - } - return nil - }, timeout).Should(Succeed()) + Expect(instance.Status.IgnoredFiles).To(HaveLen(int(instance.Status.ObjectsIgnored))) }) }) - Context("and the subPath has changed", func() { - }) + Context("listObjectsByName", func() { + var reconciler *ReconcileGitTrack + var children map[string]farosv1alpha1.GitTrackObjectInterface - Context("and the reference is invalid", func() { BeforeEach(func() { - createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + var ok bool + reconciler, ok = r.(*ReconcileGitTrack) + Expect(ok).To(BeTrue()) + + createInstance(instance, "b17c0e0f45beca3f1c1e62a7f49fecb738c60d42") // Wait for client cache to expire waitForInstanceCreated(key) - }) - - It("does not update any of the resources", func() { - deployBefore, deployAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - serviceBefore, serviceAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployBefore) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceBefore) - }, timeout).Should(Succeed()) - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + var err error + children, err = reconciler.listObjectsByName(instance) + Expect(err).NotTo(HaveOccurred()) + }) - instance.Spec.Reference = doesNotExistPath - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployAfter) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceAfter) - }, timeout).Should(Succeed()) - - Expect(deployBefore).To(Equal(deployAfter)) - Expect(serviceBefore).To(Equal(serviceAfter)) + It("should return 6 child objects", func() { + Expect(children).Should(HaveLen(6)) }) - It("updates the FilesFetched condition", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - instance.Spec.Reference = doesNotExistPath - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - Eventually(func() error { - err = c.Get(context.TODO(), key, instance) - if err != nil { - return err + It("should return 5 namespaced objects", func() { + var count int + for _, obj := range children { + if _, ok := obj.(*farosv1alpha1.GitTrackObject); ok { + count++ } - c := instance.Status.Conditions[1] - if c.Status != v1.ConditionFalse { - return fmt.Errorf("condition hasn't updated yet") - } - return nil - }, timeout).Should(Succeed()) - // TODO: don't rely on ordering - c := instance.Status.Conditions[1] - Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) - Expect(c.Message).To(Equal("failed to checkout 'does-not-exist': unable to parse ref does-not-exist: reference not found")) + } + Expect(count).To(Equal(5)) }) - It("sends a CheckoutFailed event", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - instance.Spec.Reference = doesNotExistPath - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - Eventually(func() error { - events := &v1.EventList{} - err := c.List(context.TODO(), events) - if err != nil { - return err + It("should return 1 non-namespaced resource", func() { + var count int + for _, obj := range children { + if _, ok := obj.(*farosv1alpha1.ClusterGitTrackObject); ok { + count++ } - if testevents.None(events.Items, reasonFilter("CheckoutFailed")) { - return fmt.Errorf("events hasn't been sent yet") - } - return nil - }, timeout).Should(Succeed()) - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - failedEvents := testevents.Select(events.Items, reasonFilter("CheckoutFailed")) - Expect(failedEvents).ToNot(BeEmpty()) - for _, e := range failedEvents { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Type).To(Equal(v1.EventTypeWarning)) } + Expect(count).To(Equal(1)) }) - }) - Context("and the subPath is invalid", func() { - BeforeEach(func() { - createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") - // Wait for client cache to expire - waitForInstanceCreated(key) + It("should key all items by their NamespacedName", func() { + for key, obj := range children { + Expect(key).Should(Equal(obj.GetNamespacedName())) + } }) + }) - It("does not update any of the resources", func() { - deployBefore, deployAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - serviceBefore, serviceAfter := &farosv1alpha1.GitTrackObject{}, &farosv1alpha1.GitTrackObject{} - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployBefore) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceBefore) - }, timeout).Should(Succeed()) - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Context("when cross-namespace ownership disabled", func() { - instance.Spec.SubPath = doesNotExistPath - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployAfter) - }, timeout).Should(Succeed()) - Eventually(func() error { - return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceAfter) - }, timeout).Should(Succeed()) - - Expect(deployBefore).To(Equal(deployAfter)) - Expect(serviceBefore).To(Equal(serviceAfter)) + BeforeEach(func() { + farosflags.AllowCrossNamespaceOwnership = false }) - It("updates the FilesFetched condition", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - instance.Spec.SubPath = doesNotExistPath - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - Eventually(func() error { - err = c.Get(context.TODO(), key, instance) - if err != nil { - return err + Context("listObjectsByName", func() { + var reconciler *ReconcileGitTrack + var children map[string]farosv1alpha1.GitTrackObjectInterface + + BeforeEach(func() { + var ok bool + reconciler, ok = r.(*ReconcileGitTrack) + Expect(ok).To(BeTrue()) + + createInstance(instance, "b17c0e0f45beca3f1c1e62a7f49fecb738c60d42") + // Wait for client cache to expire + waitForInstanceCreated(key) + + var err error + children, err = reconciler.listObjectsByName(instance) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should return 5 child objects", func() { + Expect(children).Should(HaveLen(5)) + }) + + It("should return 5 namespaced objects", func() { + var count int + for _, obj := range children { + if _, ok := obj.(*farosv1alpha1.GitTrackObject); ok { + count++ + } } - c := instance.Status.Conditions[1] - if c.Status != v1.ConditionFalse { - return fmt.Errorf("condition hasn't updated yet") + Expect(count).To(Equal(5)) + }) + + It("should return no non-namespaced resource", func() { + var count int + for _, obj := range children { + if _, ok := obj.(*farosv1alpha1.ClusterGitTrackObject); ok { + count++ + } } - return nil - }, timeout).Should(Succeed()) - // TODO: don't rely on ordering - c := instance.Status.Conditions[1] - Expect(c.Type).To(Equal(farosv1alpha1.FilesFetchedType)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ErrorFetchingFiles))) - Expect(c.Message).To(Equal("no files for subpath 'does-not-exist'")) - }) + Expect(count).To(Equal(0)) + }) - It("sends a CheckoutFailed event", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - instance.Spec.SubPath = doesNotExistPath - err := c.Update(context.TODO(), instance) - Expect(err).ToNot(HaveOccurred()) - // Wait for reconcile for update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - // Wait for reconcile for status update - Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) - filter := func(e v1.Event) bool { - return e.Reason == "CheckoutFailed" && e.Message == "No files for SubPath 'does-not-exist'" - } - Eventually(func() error { - events := &v1.EventList{} - err := c.List(context.TODO(), events) - if err != nil { - return err + It("should key all items by their NamespacedName", func() { + for key, obj := range children { + Expect(key).Should(Equal(obj.GetNamespacedName())) } - if testevents.None(events.Items, filter) { - return fmt.Errorf("events hasn't been sent yet") - } - return nil - }, timeout).Should(Succeed()) - events := &v1.EventList{} - Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) - failedEvents := testevents.Select(events.Items, filter) - Expect(failedEvents).ToNot(BeEmpty()) - for _, e := range failedEvents { - Expect(e.InvolvedObject.Kind).To(Equal("GitTrack")) - Expect(e.InvolvedObject.Name).To(Equal("example")) - Expect(e.Message).To(Equal("No files for SubPath 'does-not-exist'")) - Expect(e.Type).To(Equal(string(v1.EventTypeWarning))) - } + }) }) }) }) - Context("When a GitTrack resource is deleted", func() { - }) + Context("ClusterGitTrack", func() { + var key = types.NamespacedName{Name: "example"} + var expectedRequest = reconcile.Request{NamespacedName: key} + var instance *farosv1alpha2.ClusterGitTrack - Context("When a GitTrack has a DeployKey, the Reconciler should", func() { - var reconciler *ReconcileGitTrack - var s *v1.Secret - var keyRef farosv1alpha1.GitTrackDeployKey - var expectedKey []byte + var createInstance = func(gt *farosv1alpha2.ClusterGitTrack, ref string) { + gt.Spec.Reference = ref + err := c.Create(context.TODO(), gt) + Expect(err).NotTo(HaveOccurred()) + } - keysMustBeSetErr := errors.New("if using a deploy key, both SecretName and Key must be set") - secretNotFoundErr := errors.New("failed to look up secret nonExistSecret: Secret \"nonExistSecret\" not found") + var waitForInstanceCreated = func(key types.NamespacedName) { + request := reconcile.Request{NamespacedName: key} + // wait for reconcile for creating the GitTrack resource + Eventually(requests, timeout).Should(Receive(Equal(request))) + // wait for reconcile for updating the GitTrack resource's status + Eventually(requests, timeout).Should(Receive(Equal(request))) + obj := &farosv1alpha2.ClusterGitTrack{} + Eventually(func() error { + err := c.Get(context.TODO(), key, obj) + if err != nil { + return err + } + if len(obj.Status.Conditions) == 0 { + return fmt.Errorf("Status not updated") + } + return nil + }, timeout).Should(Succeed()) + } BeforeEach(func() { - var ok bool - reconciler, ok = r.(*ReconcileGitTrack) - Expect(ok).To(BeTrue()) - - keyRef = farosv1alpha1.GitTrackDeployKey{ - SecretName: "foosecret", - Key: "privatekey", - } - - expectedKey = []byte("PrivateKey") - s = &v1.Secret{ + // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a + // channel when it is finished. + var err error + cfg.RateLimiter = flowcontrol.NewFakeAlwaysRateLimiter() + mgr, err = manager.New(cfg, manager.Options{ + Namespace: farosflags.Namespace, + MetricsBindAddress: "0", // Disable serving metrics while testing + }) + Expect(err).NotTo(HaveOccurred()) + c = mgr.GetClient() + + var recFn reconcile.Reconciler + r = newReconciler(mgr) + recFn, requests = SetupTestReconcile(r) + Expect(add(mgr, recFn)).NotTo(HaveOccurred()) + stop = StartTestManager(mgr) + instance = &farosv1alpha2.ClusterGitTrack{ ObjectMeta: metav1.ObjectMeta{ - Name: "foosecret", - Namespace: "default", + Name: "example", }, - Data: map[string][]byte{ - "privatekey": expectedKey, + Spec: farosv1alpha1.GitTrackSpec{ + Repository: repositoryURL, }, } - Expect(c.Create(context.TODO(), s)).NotTo(HaveOccurred()) }) AfterEach(func() { - c.Delete(context.TODO(), s) - }) - - It("do nothing if the secret name and key are empty", func() { - key, err := reconciler.fetchGitCredentials("default", farosv1alpha1.GitTrackDeployKey{}) - Expect(err).NotTo(HaveOccurred()) - Expect(key).To(BeNil()) - }) - - It("get the key from the secret", func() { - key, err := reconciler.fetchGitCredentials("default", keyRef) - Expect(err).NotTo(HaveOccurred()) - Expect(key.secret).To(Equal(expectedKey)) - }) - - It("return an error if the secret doesn't exist", func() { - keyRef.SecretName = "nonExistSecret" - key, err := reconciler.fetchGitCredentials("default", keyRef) - Expect(err).To(Equal(secretNotFoundErr)) - Expect(key).To(BeNil()) - }) - - It("return an error if the secret name isnt set, but the key is", func() { - keyRef.SecretName = "" - key, err := reconciler.fetchGitCredentials("default", keyRef) - Expect(err).To(Equal(keysMustBeSetErr)) - Expect(key).To(BeNil()) - }) - - It("return an error if the key isnt set, but the secret name is", func() { - keyRef.Key = "" - key, err := reconciler.fetchGitCredentials("default", keyRef) - Expect(err).To(Equal(keysMustBeSetErr)) - Expect(key).To(BeNil()) - }) - }) - - Context("When getting files from a repository", func() { - /* - foo - ├── bar - │   ├── non-yaml-file.txt - │   └── service.yaml - ├── deployment.yaml - └── namespace.yaml - */ - - getsFilesFromRepo("foo", 3) - getsFilesFromRepo("foo/", 3) - getsFilesFromRepo("/foo/", 3) - getsFilesFromRepo("foo/bar", 1) - getsFilesFromRepo("foobar", 2) - getsFilesFromRepo("foobar/", 2) - }) - - Context(fmt.Sprintf("with invalid files"), func() { - BeforeEach(func() { - createInstance(instance, "936b7ee3df1dbd61b1fc691b742fa5d5d3c0dced") - waitForInstanceCreated(key) - }) - - It("adds a message to the ignoredFiles status", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("invalid_file.yaml", "unable to parse 'invalid_file.yaml': unable to unmarshal JSON: Object 'Kind' is missing in '{\"I\":\"a;m an \\\"invalid Kubernetes manifest.)\"}'\n")) - }) - - It("includes the invalid file in ignoredObjects count", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.IgnoredFiles).To(HaveLen(int(instance.Status.ObjectsIgnored))) - }) - }) - - Context("When a list of ignored GVRs is supplied", func() { - BeforeEach(func() { - reconciler, ok := r.(*ReconcileGitTrack) - Expect(ok).To(BeTrue()) - reconciler.ignoredGVRs = make(map[schema.GroupVersionResource]interface{}) - deploymentGVR := schema.GroupVersionResource{ - Group: "apps", - Version: "v1", - Resource: "deployments", - } - reconciler.ignoredGVRs[deploymentGVR] = nil - - createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") - // Wait for client cache to expire - waitForInstanceCreated(key) + close(stop) + testutils.DeleteAll(cfg, timeout, + &farosv1alpha1.GitTrackList{}, + &farosv1alpha2.ClusterGitTrackList{}, + &farosv1alpha1.GitTrackObjectList{}, + &farosv1alpha1.ClusterGitTrackObjectList{}, + &v1.EventList{}, + ) }) - It("ignores the deployment files", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - - // TODO: don't rely on ordering - c := instance.Status.Conditions[3] - Expect(c.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) - Expect(c.Status).To(Equal(v1.ConditionTrue)) - Expect(c.LastUpdateTime).NotTo(BeNil()) - Expect(c.LastTransitionTime).NotTo(BeNil()) - Expect(c.LastUpdateTime).To(Equal(c.LastTransitionTime)) - Expect(c.Reason).To(Equal(string(gittrackutils.ChildrenUpdateSuccess))) + Context("When a ClusterGitTrack resource is created", func() { + Context("with a valid Spec", func() { + BeforeEach(func() { + createInstance(instance, "a14443638218c782b84cae56a14f1090ee9e5c9c") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("updates its status", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + two, zero := int64(2), int64(0) + Expect(instance.Status.ObjectsDiscovered).To(Equal(two)) + Expect(instance.Status.ObjectsApplied).To(Equal(two)) + Expect(instance.Status.ObjectsIgnored).To(Equal(zero)) + Expect(instance.Status.ObjectsInSync).To(Equal(zero)) + + deployGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + + now := metav1.NewTime(time.Now()) + deployGto.Status.Conditions = []farosv1alpha1.GitTrackObjectCondition{ + { + Type: farosv1alpha1.ObjectInSyncType, + Status: v1.ConditionTrue, + LastTransitionTime: now, + LastUpdateTime: now, + }, + } + Expect(c.Update(context.TODO(), deployGto)).ToNot(HaveOccurred()) + // Wait for reconcile for update + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + // Wait for reconcile for status + Eventually(requests, timeout).Should(Receive(Equal(expectedRequest))) + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + Expect(instance.Status.ObjectsInSync).To(Equal(int64(1))) + }) + + It("sets the status conditions", func() { + Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) + conditions := instance.Status.Conditions + Expect(len(conditions)).To(Equal(4)) + parseErrorCondition := conditions[0] + gitErrorCondition := conditions[1] + gcErrorCondition := conditions[2] + upToDateCondiiton := conditions[3] + Expect(parseErrorCondition.Type).To(Equal(farosv1alpha1.FilesParsedType)) + Expect(gitErrorCondition.Type).To(Equal(farosv1alpha1.FilesFetchedType)) + Expect(gcErrorCondition.Type).To(Equal(farosv1alpha1.ChildrenGarbageCollectedType)) + Expect(upToDateCondiiton.Type).To(Equal(farosv1alpha1.ChildrenUpToDateType)) + }) + + Context("sets the status metrics", func() { + var setsMetric = func(status string, value float64) { + It(fmt.Sprintf("sets status `%s` to %f", status, value), func() { + var gauge prometheus.Gauge + Eventually(func() error { + var err error + gauge, err = metrics.ChildStatus.GetMetricWith(map[string]string{ + "name": instance.GetName(), + "namespace": instance.GetNamespace(), + "status": status, + }) + return err + }, timeout).Should(Succeed()) + var metric dto.Metric + Expect(gauge.Write(&metric)).NotTo(HaveOccurred()) + Expect(metric.GetGauge().GetValue()).To(Equal(value)) + }) + } - Expect(instance.Status.ObjectsIgnored).To(Equal(int64(1))) - }) + setsMetric("discovered", 2.0) + setsMetric("applied", 2.0) + setsMetric("ignored", 0.0) + setsMetric("inSync", 0.0) + }) + + It("creates GitTrackObjects", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + serviceGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) + }, timeout).Should(Succeed()) + }) + + It("sets ownerReferences for created GitTrackObjects", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + serviceGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) + }, timeout).Should(Succeed()) + Expect(len(deployGto.OwnerReferences)).To(Equal(1)) + Expect(len(serviceGto.OwnerReferences)).To(Equal(1)) + }) + + It("sets LastAppliedAnnotations for created GitTrackObjects", func() { + deployGto := &farosv1alpha1.GitTrackObject{} + serviceGto := &farosv1alpha1.GitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "deployment-nginx", Namespace: "default"}, deployGto) + }, timeout).Should(Succeed()) + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "service-nginx", Namespace: "default"}, serviceGto) + }, timeout).Should(Succeed()) + Expect(deployGto.GetAnnotations()).To(HaveKey(farosclient.LastAppliedAnnotation)) + Expect(serviceGto.GetAnnotations()).To(HaveKey(farosclient.LastAppliedAnnotation)) + }) + + It("sends events about checking out configured Git repository", func() { + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + startEvents := testevents.Select(events.Items, reasonFilter("CheckoutStarted")) + successEvents := testevents.Select(events.Items, reasonFilter("CheckoutSuccessful")) + Expect(startEvents).ToNot(BeEmpty()) + Expect(successEvents).ToNot(BeEmpty()) + for _, e := range append(startEvents, successEvents...) { + Expect(e.InvolvedObject.Kind).To(Equal("ClusterGitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) + } + }) - It("adds a message to the ignoredFiles status", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.IgnoredFiles).To(HaveKeyWithValue("default/deployment-nginx", "resource `deployments.apps/v1` ignored globally by flag")) - }) + It("sends events about creating GitTrackObjects", func() { + events := &v1.EventList{} + Eventually(func() error { return c.List(context.TODO(), events) }, timeout).Should(Succeed()) + startEvents := testevents.Select(events.Items, reasonFilter("CreateStarted")) + successEvents := testevents.Select(events.Items, reasonFilter("CreateSuccessful")) + Expect(startEvents).ToNot(BeEmpty()) + Expect(successEvents).ToNot(BeEmpty()) + for _, e := range append(startEvents, successEvents...) { + Expect(e.InvolvedObject.Kind).To(Equal("ClusterGitTrack")) + Expect(e.InvolvedObject.Name).To(Equal("example")) + Expect(e.Type).To(Equal(string(v1.EventTypeNormal))) + } + }) + }) - It("includes the ignored files in ignoredObjects count", func() { - Eventually(func() error { return c.Get(context.TODO(), key, instance) }, timeout).Should(Succeed()) - Expect(instance.Status.IgnoredFiles).To(HaveLen(int(instance.Status.ObjectsIgnored))) + Context("with a child resource that has a name that contains `:`", func() { + BeforeEach(func() { + createInstance(instance, "241786090da55894dca4e91e3f5023c024d3d9a8") + // Wait for client cache to expire + waitForInstanceCreated(key) + }) + + It("replaces `:` with `-`", func() { + clusterRoleGto := &farosv1alpha1.ClusterGitTrackObject{} + Eventually(func() error { + return c.Get(context.TODO(), types.NamespacedName{Name: "clusterrole-test-read-ns-pods-svcs"}, clusterRoleGto) + }, timeout).Should(Succeed()) + Expect(clusterRoleGto.Name).To(Equal("clusterrole-test-read-ns-pods-svcs")) + }) + }) }) - }) - Context("listObjectsByName", func() { - var reconciler *ReconcileGitTrack - var children map[string]farosv1alpha1.GitTrackObjectInterface + Context("listObjectsByName", func() { + var reconciler *ReconcileGitTrack + var children map[string]farosv1alpha1.GitTrackObjectInterface - BeforeEach(func() { - var ok bool - reconciler, ok = r.(*ReconcileGitTrack) - Expect(ok).To(BeTrue()) + BeforeEach(func() { + var ok bool + reconciler, ok = r.(*ReconcileGitTrack) + Expect(ok).To(BeTrue()) - createInstance(instance, "b17c0e0f45beca3f1c1e62a7f49fecb738c60d42") - // Wait for client cache to expire - waitForInstanceCreated(key) + createInstance(instance, "b17c0e0f45beca3f1c1e62a7f49fecb738c60d42") + // Wait for client cache to expire + waitForInstanceCreated(key) - var err error - children, err = reconciler.listObjectsByName(instance) - Expect(err).NotTo(HaveOccurred()) - }) + var err error + children, err = reconciler.listObjectsByName(instance) + Expect(err).NotTo(HaveOccurred()) + }) - It("should return 6 child objects", func() { - Expect(children).Should(HaveLen(6)) - }) + It("should return 6 child objects", func() { + Expect(children).Should(HaveLen(6)) + }) - It("should return 5 namespaced objects", func() { - var count int - for _, obj := range children { - if _, ok := obj.(*farosv1alpha1.GitTrackObject); ok { - count++ + It("should return 5 namespaced objects", func() { + var count int + for _, obj := range children { + if _, ok := obj.(*farosv1alpha1.GitTrackObject); ok { + count++ + } } - } - Expect(count).To(Equal(5)) - }) + Expect(count).To(Equal(5)) + }) - It("should return 1 non-namespaced resource", func() { - var count int - for _, obj := range children { - if _, ok := obj.(*farosv1alpha1.ClusterGitTrackObject); ok { - count++ + It("should return 1 non-namespaced resource", func() { + var count int + for _, obj := range children { + if _, ok := obj.(*farosv1alpha1.ClusterGitTrackObject); ok { + count++ + } } - } - Expect(count).To(Equal(1)) - }) + Expect(count).To(Equal(1)) + }) - It("should key all items by their NamespacedName", func() { - for key, obj := range children { - Expect(key).Should(Equal(obj.GetNamespacedName())) - } + It("should key all items by their NamespacedName", func() { + for key, obj := range children { + Expect(key).Should(Equal(obj.GetNamespacedName())) + } + }) }) }) }) diff --git a/pkg/controller/gittrack/metrics.go b/pkg/controller/gittrack/metrics.go index 6634cc4e9..e157a7be7 100644 --- a/pkg/controller/gittrack/metrics.go +++ b/pkg/controller/gittrack/metrics.go @@ -37,7 +37,7 @@ func newMetricOpts(status *statusOpts) *metricsOpts { } } -func (r *ReconcileGitTrack) updateMetrics(gt *farosv1alpha1.GitTrack, opts *metricsOpts) error { +func (r *ReconcileGitTrack) updateMetrics(gt farosv1alpha1.GitTrackInterface, opts *metricsOpts) error { if gt == nil { return nil } diff --git a/pkg/controller/gittrack/status.go b/pkg/controller/gittrack/status.go index a1b3ed29a..8c3602c31 100644 --- a/pkg/controller/gittrack/status.go +++ b/pkg/controller/gittrack/status.go @@ -51,12 +51,12 @@ func newStatusOpts() *statusOpts { } } -func updateGitTrackStatus(gt *farosv1alpha1.GitTrack, opts *statusOpts) (updated bool) { +func updateGitTrackStatus(gt farosv1alpha1.GitTrackInterface, opts *statusOpts) (updated bool) { if gt == nil { return } - status := gt.Status + status := gt.GetStatus() status.ObjectsApplied = opts.applied status.ObjectsDiscovered = opts.discovered @@ -68,8 +68,8 @@ func updateGitTrackStatus(gt *farosv1alpha1.GitTrack, opts *statusOpts) (updated setCondition(&status, farosv1alpha1.ChildrenGarbageCollectedType, opts.gcError, opts.gcReason) setCondition(&status, farosv1alpha1.ChildrenUpToDateType, opts.upToDateError, opts.upToDateReason) - if !reflect.DeepEqual(gt.Status, status) { - gt.Status = status + if !reflect.DeepEqual(gt.GetStatus(), status) { + gt.SetStatus(status) updated = true } return @@ -100,9 +100,9 @@ func setCondition(status *farosv1alpha1.GitTrackStatus, condType farosv1alpha1.G // updateStatus calculates a new status for the GitTrack and then updates // the resource on the API if the status differs from before. -func (r *ReconcileGitTrack) updateStatus(original *farosv1alpha1.GitTrack, opts *statusOpts) error { +func (r *ReconcileGitTrack) updateStatus(original farosv1alpha1.GitTrackInterface, opts *statusOpts) error { // Update the GitTrack's status - gt := original.DeepCopy() + gt := original.DeepCopyInterface() gtUpdated := updateGitTrackStatus(gt, opts) // If the status was modified, update the GitTrack on the API diff --git a/pkg/flags/flagset.go b/pkg/flags/flagset.go index 61ac869c1..b2d183209 100644 --- a/pkg/flags/flagset.go +++ b/pkg/flags/flagset.go @@ -36,6 +36,9 @@ var ( // ServerDryRun whether to enable Server side dry run or not ServerDryRun bool + + // AllowCrossNamespaceOwnership defines whether cross-namespace ownership is allowed. + AllowCrossNamespaceOwnership bool ) func init() { @@ -43,6 +46,7 @@ func init() { FlagSet.StringVar(&Namespace, "namespace", "", "Only manage GitTrack resources in given namespace") FlagSet.StringSliceVar(&ignoredResources, "ignore-resource", []string{}, "Ignore resources of these kinds found in repositories, specified in ./ format eg jobs.batch/v1") FlagSet.BoolVar(&ServerDryRun, "server-dry-run", true, "Enable/Disable server side dry run before updating resources") + FlagSet.BoolVar(&AllowCrossNamespaceOwnership, "allow-cross-namespace-ownership", true, "Whether cross-namespace ownership should be allowed. Enable with caution! See #143") } // ParseIgnoredResources attempts to parse the ignore-resource flag value and diff --git a/pkg/utils/predicate.go b/pkg/utils/predicate.go index ed16127f4..6910bfb23 100644 --- a/pkg/utils/predicate.go +++ b/pkg/utils/predicate.go @@ -20,13 +20,15 @@ import ( "context" farosv1alpha1 "github.com/pusher/faros/pkg/apis/faros/v1alpha1" + farosv1alpha2 "github.com/pusher/faros/pkg/apis/faros/v1alpha2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" ) const ( - farosGroupVersion = "faros.pusher.com/v1alpha1" + farosGroupVersionAlpha1 = "faros.pusher.com/v1alpha1" + farosGroupVersionAlpha2 = "faros.pusher.com/v1alpha2" ) // OwnerInNamespacePredicate filters events to check the owner of the event @@ -63,20 +65,34 @@ func (p OwnerInNamespacePredicate) Generic(e event.GenericEvent) bool { // When it is restricted to a namespace this should only be the GitTracks // in the namespace the controller is managing. func (p OwnerInNamespacePredicate) ownerInNamespace(ownerRefs []metav1.OwnerReference) bool { + cgtList := &farosv1alpha2.ClusterGitTrackList{} + err := p.client.List(context.TODO(), cgtList) + if err != nil { + // We can't list CGTOs so fail closed and ignore the requests + return false + } gtList := &farosv1alpha1.GitTrackList{} - err := p.client.List(context.TODO(), gtList) + err = p.client.List(context.TODO(), gtList) if err != nil { // We can't list CGTOs so fail closed and ignore the requests return false } + for _, ref := range ownerRefs { - if ref.Kind == "GitTrack" && ref.APIVersion == farosGroupVersion { + if ref.Kind == "GitTrack" && ref.APIVersion == farosGroupVersionAlpha1 { for _, gt := range gtList.Items { if ref.UID == gt.UID { return true } } } + if ref.Kind == "ClusterGitTrack" && ref.APIVersion == farosGroupVersionAlpha2 { + for _, cgt := range cgtList.Items { + if ref.UID == cgt.UID { + return true + } + } + } } return false } @@ -116,7 +132,7 @@ func (p OwnersOwnerInNamespacePredicate) Generic(e event.GenericEvent) bool { } // ownersOwnerInNamespace returns true if the the GitTrackObject's GitTrack -// owner of the event object is in the namespace managed by the controller +// owner of the event object is in the namespace managed by the controller // // This works on the premise that listing objects from the client will only // return those in its cache. @@ -137,14 +153,14 @@ func (p OwnersOwnerInNamespacePredicate) ownersOwnerInNamespace(ownerRefs []meta } for _, ref := range ownerRefs { - if ref.Kind == "GitTrackObject" && ref.APIVersion == farosGroupVersion { + if ref.Kind == "GitTrackObject" && ref.APIVersion == farosGroupVersionAlpha1 { for _, gto := range gtoList.Items { if ref.UID == gto.UID { return p.ownerInNamespacePredicate.ownerInNamespace(gto.GetOwnerReferences()) } } } - if ref.Kind == "ClusterGitTrackObject" && ref.APIVersion == farosGroupVersion { + if ref.Kind == "ClusterGitTrackObject" && ref.APIVersion == farosGroupVersionAlpha1 { for _, cgto := range cgtoList.Items { if ref.UID == cgto.UID { return p.ownerInNamespacePredicate.ownerInNamespace(cgto.GetOwnerReferences())