Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DEVHAS-488]annotation to force generate gitops #410

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions controllers/application_controller_finalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func containsString(slice []string, s string) bool {
return false
}

// getApplicationFailCount gets the given counter annotation on the resource (defaults to 0 if unset)
// getCounterAnnotation gets the given counter annotation on the resource (defaults to 0 if unset)
func getCounterAnnotation(annotation string, obj client.Object) (int, error) {
objAnnotations := obj.GetAnnotations()
if objAnnotations == nil || objAnnotations[annotation] == "" {
Expand All @@ -100,7 +100,7 @@ func getCounterAnnotation(annotation string, obj client.Object) (int, error) {
return strconv.Atoi(counterAnnotation)
}

// setApplicationFailCount sets the given counter annotation on the resource to the specified value
// setCounterAnnotation sets the given counter annotation on the resource to the specified value
func setCounterAnnotation(annotation string, obj client.Object, count int) {
objAnnotations := obj.GetAnnotations()
if objAnnotations == nil {
Expand Down
25 changes: 23 additions & 2 deletions controllers/component_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
applicationFailCounterAnnotation = "applicationFailCounter"
maxApplicationFailCount = 5
componentName = "Component"
forceGenerationAnnotation = "forceGitopsGeneration"
)

//+kubebuilder:rbac:groups=appstudio.redhat.com,resources=components,verbs=get;list;watch;create;update;patch;delete
Expand Down Expand Up @@ -131,6 +132,8 @@
}

setCounterAnnotation(applicationFailCounterAnnotation, &component, 0)
forceGenerateGitopsResource := getForceGenerateGitopsAnnotation(component)
log.Info(fmt.Sprintf("forceGenerateGitopsResource is %v", forceGenerateGitopsResource))

ghClient, err := r.GitHubTokenClient.GetNewGitHubClient("")
if err != nil {
Expand Down Expand Up @@ -180,7 +183,7 @@
log.Info(fmt.Sprintf("added the finalizer %v", req.NamespacedName))
}
} else {
if hasApplication.Status.Devfile != "" && len(component.Status.Conditions) > 0 && component.Status.Conditions[len(component.Status.Conditions)-1].Status == metav1.ConditionTrue && containsString(component.GetFinalizers(), compFinalizerName) {
if hasApplication.Status.Devfile != "" && (forceGenerateGitopsResource || len(component.Status.Conditions) > 0 && component.Status.Conditions[len(component.Status.Conditions)-1].Status == metav1.ConditionTrue && containsString(component.GetFinalizers(), compFinalizerName)) {
// only attempt to finalize and update the gitops repo if an Application is present & the previous Component status is good
// A finalizer is present for the Component CR, so make sure we do the necessary cleanup steps
metrics.ComponentDeletionTotalReqs.Inc()
Expand Down Expand Up @@ -217,7 +220,7 @@
isUpdateConditionPresent := false
isGitOpsRegenSuccessful := false
for _, condition := range component.Status.Conditions {
if condition.Type == "GitOpsResourcesGenerated" && condition.Reason == "GenerateError" && condition.Status == metav1.ConditionFalse {
if forceGenerateGitopsResource || (condition.Type == "GitOpsResourcesGenerated" && condition.Reason == "GenerateError" && condition.Status == metav1.ConditionFalse) {
log.Info(fmt.Sprintf("Re-attempting GitOps generation for %s", component.Name))
// Parse the Component Devfile
compDevfileData, err := cdqanalysis.ParseDevfileWithParserArgs(&devfileParser.ParserArgs{Data: []byte(component.Status.Devfile), Token: gitToken})
Expand All @@ -238,6 +241,7 @@
if err != nil {
return ctrl.Result{}, err
}
setForceGenerateGitopsAnnotation(&component, "false")
isGitOpsRegenSuccessful = true
}
} else if condition.Type == "Updated" && condition.Reason == "Error" && condition.Status == metav1.ConditionFalse {
Expand Down Expand Up @@ -308,8 +312,8 @@
if gitToken == "" {
gitURL, err = cdqanalysis.ConvertGitHubURL(source.GitSource.URL, source.GitSource.Revision, context)
if err != nil {
// ConvertGitHubURL only returns user error
metrics.ImportGitRepoSucceeded.Inc()

Check warning on line 316 in controllers/component_controller.go

View check run for this annotation

Codecov / codecov/patch

controllers/component_controller.go#L315-L316

Added lines #L315 - L316 were not covered by tests
log.Error(err, fmt.Sprintf("Unable to convert Github URL to raw format, exiting reconcile loop %v", req.NamespacedName))
_ = r.SetCreateConditionAndUpdateCR(ctx, req, &component, err)
return ctrl.Result{}, err
Expand All @@ -331,13 +335,13 @@
// Use SPI to retrieve the devfile from the private repository
devfileBytes, devfileLocation, err = spi.DownloadDevfileUsingSPI(r.SPIClient, ctx, component, gitURL, source.GitSource.Revision, context)
if err != nil {
// Increment the import git repo failed metric on non-user errors
if _, ok := err.(*devfile.NoFileFound); !ok {
metrics.ImportGitRepoFailed.Inc()
} else {
metrics.ImportGitRepoSucceeded.Inc()
}
log.Error(err, fmt.Sprintf("Unable to download from any known devfile locations from %s %v", gitURL, req.NamespacedName))

Check warning on line 344 in controllers/component_controller.go

View check run for this annotation

Codecov / codecov/patch

controllers/component_controller.go#L338-L344

Added lines #L338 - L344 were not covered by tests
_ = r.SetCreateConditionAndUpdateCR(ctx, req, &component, err)
return ctrl.Result{}, err
}
Expand All @@ -345,10 +349,10 @@

gitURL, err := cdqanalysis.ConvertGitHubURL(source.GitSource.URL, source.GitSource.Revision, context)
if err != nil {
log.Error(err, fmt.Sprintf("Unable to convert Github URL to raw format, exiting reconcile loop %v", req.NamespacedName))
_ = r.SetCreateConditionAndUpdateCR(ctx, req, &component, err)
return ctrl.Result{}, err
}

Check warning on line 355 in controllers/component_controller.go

View check run for this annotation

Codecov / codecov/patch

controllers/component_controller.go#L352-L355

Added lines #L352 - L355 were not covered by tests
devfileLocation = gitURL + string(os.PathSeparator) + devfileLocation
}

Expand Down Expand Up @@ -610,16 +614,16 @@
if err != nil {
retErr := err
if strings.Contains(strings.ToLower(err.Error()), "github push protection") {
retErr = fmt.Errorf("potential secret leak caught by github push protection")
// to get the URL token
// e.g. <GitURL>/security/secret-scanning/unblock-secret/2WlUv72plUf05tgshlpRLzSlH4R \n
var unblockURL string
splited := strings.Split(strings.ToLower(err.Error()), "unblock-secret/")
if len(splited) > 1 {
token := strings.Split(splited[1], " ")[0]
unblockURL = fmt.Sprintf("%v/security/secret-scanning/unblock-secret/%v", component.Status.GitOps.RepositoryURL, token)
log.Error(retErr, fmt.Sprintf("unable to generate gitops resources due to git push protecton error, follow the link to unblock the secret: %v", unblockURL))
}

Check warning on line 626 in controllers/component_controller.go

View check run for this annotation

Codecov / codecov/patch

controllers/component_controller.go#L617-L626

Added lines #L617 - L626 were not covered by tests
} else {
log.Error(retErr, "unable to generate gitops resources due to error")
}
Expand All @@ -631,23 +635,23 @@
metrics.ControllerGitRequest.With(prometheus.Labels{"controller": componentName, "tokenName": ghClient.TokenName, "operation": "CommitAndPush"}).Inc()
err = r.Generator.CommitAndPush(tempDir, "", gitOpsURL, mappedGitOpsComponent.Name, gitOpsBranch, "Generating GitOps resources")
if err != nil {
retErr := err
if strings.Contains(strings.ToLower(err.Error()), "github push protection") {
retErr = fmt.Errorf("potential secret leak caught by github push protection")
// to get the URL token
// e.g. <GitURL>/security/secret-scanning/unblock-secret/2WlUv72plUf05tgshlpRLzSlH4R \n
var unblockURL string
splited := strings.Split(strings.ToLower(err.Error()), "unblock-secret/")
if len(splited) > 1 {
token := strings.Split(splited[1], " ")[0]
unblockURL = fmt.Sprintf("%v/security/secret-scanning/unblock-secret/%v", component.Status.GitOps.RepositoryURL, token)
log.Error(retErr, fmt.Sprintf("unable to commit and push gitops resources due to git push protecton error, follow the link to unblock the secret: %v", unblockURL))
}
} else {
log.Error(retErr, "unable to commit and push gitops resources due to error")
}

Check warning on line 652 in controllers/component_controller.go

View check run for this annotation

Codecov / codecov/patch

controllers/component_controller.go#L638-L652

Added lines #L638 - L652 were not covered by tests
ioutils.RemoveFolderAndLogError(log, r.AppFS, tempDir)
return retErr

Check warning on line 654 in controllers/component_controller.go

View check run for this annotation

Codecov / codecov/patch

controllers/component_controller.go#L654

Added line #L654 was not covered by tests
}

// Get the commit ID for the gitops repository
Expand Down Expand Up @@ -754,3 +758,20 @@
return ctrl.Result{}, componentErr
}
}

// getForceGenerateGitopsAnnotation gets the internal annotation on the component whether to force generate the gitops resource
func getForceGenerateGitopsAnnotation(component appstudiov1alpha1.Component) bool {
compAnnotations := component.GetAnnotations()
if compAnnotations != nil && compAnnotations[forceGenerationAnnotation] == "true" {
return true
}
return false
}

func setForceGenerateGitopsAnnotation(component *appstudiov1alpha1.Component, value string) {
compAnnotations := component.GetAnnotations()
if compAnnotations == nil {
component.Annotations = make(map[string]string)
}
component.Annotations[forceGenerationAnnotation] = value
}
10 changes: 7 additions & 3 deletions controllers/component_controller_conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,21 @@ func (r *ComponentReconciler) SetUpdateConditionAndUpdateCR(ctx context.Context,
func (r *ComponentReconciler) SetGitOpsGeneratedConditionAndUpdateCR(ctx context.Context, req ctrl.Request, component *appstudiov1alpha1.Component, generateError error) error {
log := ctrl.LoggerFrom(ctx)
condition := metav1.Condition{}

forceGenerateGitopsResource := getForceGenerateGitopsAnnotation(*component)
conditionType := "GitOpsResourcesGenerated"
if forceGenerateGitopsResource {
conditionType = "GitOpsResourcesForceGenerated"
}
if generateError == nil {
condition = metav1.Condition{
Type: "GitOpsResourcesGenerated",
Type: conditionType,
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "GitOps resource generated successfully",
}
} else {
condition = metav1.Condition{
Type: "GitOpsResourcesGenerated",
Type: conditionType,
Status: metav1.ConditionFalse,
Reason: "GenerateError",
Message: fmt.Sprintf("GitOps resources failed to generate: %v", generateError),
Expand Down
76 changes: 76 additions & 0 deletions controllers/component_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2016,6 +2016,82 @@ var _ = Describe("Component controller", func() {
deleteHASAppCR(hasAppLookupKey)
})
})
Context("force generate gitops resource", func() {
It("Should successfully update CR conditions and status", func() {
ctx := context.Background()

applicationName := HASAppName + "23"
componentName := HASCompName + "23"

createAndFetchSimpleApp(applicationName, HASAppNamespace, DisplayName, Description)

hasComp := &appstudiov1alpha1.Component{
TypeMeta: metav1.TypeMeta{
APIVersion: "appstudio.redhat.com/v1alpha1",
Kind: "Component",
},
ObjectMeta: metav1.ObjectMeta{
Name: componentName,
Namespace: HASAppNamespace,
},
Spec: appstudiov1alpha1.ComponentSpec{
ComponentName: ComponentName,
Application: applicationName,
Source: appstudiov1alpha1.ComponentSource{
ComponentSourceUnion: appstudiov1alpha1.ComponentSourceUnion{
GitSource: &appstudiov1alpha1.GitSource{
URL: SampleRepoLink,
},
},
},
},
}
Expect(k8sClient.Create(ctx, hasComp)).Should(Succeed())

// Look up the has app resource that was created.
// num(conditions) may still be < 2 (GeneratedGitOps, Created) on the first try, so retry until at least _some_ condition is set
hasCompLookupKey := types.NamespacedName{Name: componentName, Namespace: HASAppNamespace}
createdHasComp := &appstudiov1alpha1.Component{}
Eventually(func() bool {
k8sClient.Get(context.Background(), hasCompLookupKey, createdHasComp)
return len(createdHasComp.Status.Conditions) > 1 && createdHasComp.Status.GitOps.RepositoryURL != ""
}, timeout, interval).Should(BeTrue())
// Verify that the GitOpsGenerated status condition was also set
gitopsCondition := createdHasComp.Status.Conditions[len(createdHasComp.Status.Conditions)-2]
Expect(gitopsCondition.Type).To(Equal("GitOpsResourcesGenerated"))
Expect(gitopsCondition.Status).To(Equal(metav1.ConditionTrue))

// set the annotation and update a spec to force enter the reconcile
setForceGenerateGitopsAnnotation(createdHasComp, "true")
createdHasComp.Spec.TargetPort = 1111
Expect(k8sClient.Update(ctx, createdHasComp)).Should(Succeed())

createdHasComp = &appstudiov1alpha1.Component{}
// Verify that the GitOpsResourcesForceGenerated status condition was set
Eventually(func() bool {
var gitOpsForceGenerateCheck bool

k8sClient.Get(context.Background(), hasCompLookupKey, createdHasComp)
for _, condition := range createdHasComp.Status.Conditions {
if condition.Type == "GitOpsResourcesForceGenerated" && condition.Status == metav1.ConditionTrue {
gitOpsForceGenerateCheck = true
}
}
return gitOpsForceGenerateCheck
}, timeout, interval).Should(BeTrue())
gitopsCondition = createdHasComp.Status.Conditions[len(createdHasComp.Status.Conditions)-1]
Expect(gitopsCondition.Type).To(Equal("GitOpsResourcesForceGenerated"))
Expect(gitopsCondition.Status).To(Equal(metav1.ConditionTrue))

hasAppLookupKey := types.NamespacedName{Name: applicationName, Namespace: HASAppNamespace}

// Delete the specified HASComp resource
deleteHASCompCR(hasCompLookupKey)

// Delete the specified HASApp resource
deleteHASAppCR(hasAppLookupKey)
})
})

})

Expand Down
Loading