Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package digger-cli for openSUSE:Factory checked in at 2026-01-17 14:54:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/digger-cli (Old) and /work/SRC/openSUSE:Factory/.digger-cli.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "digger-cli" Sat Jan 17 14:54:07 2026 rev:45 rq:1327535 version:0.6.140 Changes: -------- --- /work/SRC/openSUSE:Factory/digger-cli/digger-cli.changes 2026-01-13 21:30:01.228688554 +0100 +++ /work/SRC/openSUSE:Factory/.digger-cli.new.1928/digger-cli.changes 2026-01-17 14:55:00.835836003 +0100 @@ -1,0 +2,6 @@ +Fri Jan 16 06:20:17 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.6.140: + * support for apply policies (#2538) + +------------------------------------------------------------------- Old: ---- digger-cli-0.6.139.obscpio New: ---- digger-cli-0.6.140.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ digger-cli.spec ++++++ --- /var/tmp/diff_new_pack.c0I6TZ/_old 2026-01-17 14:55:04.167975316 +0100 +++ /var/tmp/diff_new_pack.c0I6TZ/_new 2026-01-17 14:55:04.175975651 +0100 @@ -19,7 +19,7 @@ %define executable_name digger Name: digger-cli -Version: 0.6.139 +Version: 0.6.140 Release: 0 Summary: CLI for the digger open source IaC orchestration tool License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.c0I6TZ/_old 2026-01-17 14:55:04.363983511 +0100 +++ /var/tmp/diff_new_pack.c0I6TZ/_new 2026-01-17 14:55:04.391984682 +0100 @@ -6,7 +6,7 @@ <param name="exclude">go.mod</param> <param name="exclude">go.work</param> <param name="exclude">go.work.sum</param> - <param name="revision">v0.6.139</param> + <param name="revision">v0.6.140</param> <param name="match-tag">v*</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.c0I6TZ/_old 2026-01-17 14:55:04.491988864 +0100 +++ /var/tmp/diff_new_pack.c0I6TZ/_new 2026-01-17 14:55:04.507989533 +0100 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/diggerhq/digger</param> - <param name="changesrevision">31d84f9498d8d54bb173d086c83d6c9af314d603</param></service><service name="tar_scm"> + <param name="changesrevision">4121d16e2490ab91cf08b62ad90740902b20e628</param></service><service name="tar_scm"> <param name="url">https://github.com/johanneskastl/digger</param> <param name="changesrevision">8fe377068e53e2050ff4c745388d8428d2b13bb0</param></service></servicedata> (No newline at EOF) ++++++ digger-cli-0.6.139.obscpio -> digger-cli-0.6.140.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/backend/models/policies.go new/digger-cli-0.6.140/backend/models/policies.go --- old/digger-cli-0.6.139/backend/models/policies.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/backend/models/policies.go 2026-01-16 00:22:27.000000000 +0100 @@ -6,6 +6,7 @@ POLICY_TYPE_ACCESS = "access" POLICY_TYPE_PLAN = "plan" POLICY_TYPE_DRIFT = "drift" + POLICY_TYPE_APPLY = "apply" ) type Policy struct { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/cli/pkg/digger/digger.go new/digger-cli-0.6.140/cli/pkg/digger/digger.go --- old/digger-cli-0.6.139/cli/pkg/digger/digger.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/cli/pkg/digger/digger.go 2026-01-16 00:22:27.000000000 +0100 @@ -105,7 +105,7 @@ if !allowedToPerformCommand { msg := reportPolicyError(job.ProjectName, command, job.RequestedBy, reporter) - slog.Warn("Skipping command ... %v for project %v", command, job.ProjectName) + slog.Warn("Skipping command ...", "command", command, "projectName", job.ProjectName) slog.Warn("Received policy error", "message", msg) appliesPerProject[job.ProjectName] = false continue @@ -428,6 +428,71 @@ slog.Error(msg) return nil, msg, errors.New(msg) } + + // Check apply policy before apply + // Try to retrieve the terraform plan JSON if plan storage is configured + var terraformPlanJson string + if os.Getenv("PLAN_UPLOAD_DESTINATION") != "" { + slog.Debug("Plan storage configured, attempting to retrieve plan for apply policy check") + retrievedPlanJson, err := executor.RetrievePlanJson() + if err != nil { + slog.Warn("Failed to retrieve plan JSON for apply policy check, proceeding without plan data", "error", err) + } else { + terraformPlanJson = retrievedPlanJson + slog.Debug("Successfully retrieved plan JSON for apply policy check", "planLength", len(terraformPlanJson)) + } + } else { + slog.Debug("Plan storage not configured, apply policy will not have terraform plan data") + } + + slog.Debug("Calling CheckApplyPolicy", + "organisation", SCMOrganisation, + "repository", SCMrepository, + "projectName", job.ProjectName, + "projectDir", job.ProjectDir, + "command", command, + "requestedBy", requestedBy, + "hasTerraformPlan", terraformPlanJson != "") + allowedToApplyByApplyPolicy, applyPolicyViolations, err := policyChecker.CheckApplyPolicy(SCMOrganisation, SCMrepository, job.ProjectName, job.ProjectDir, command, job.PullRequestNumber, requestedBy, teams, approvals, approvalTeams, planPolicyViolations, terraformPlanJson) + slog.Debug("CheckApplyPolicy result", + "allowed", allowedToApplyByApplyPolicy, + "violationsCount", len(applyPolicyViolations), + "violations", applyPolicyViolations, + "error", err) + if err != nil { + msg := fmt.Sprintf("Failed to run apply policy check before apply. %v", err) + slog.Error("Failed to run apply policy check before apply", "error", err) + return nil, msg, fmt.Errorf("%s", msg) + } + if !allowedToApplyByApplyPolicy { + slog.Info("Apply policy check denied", + "violationsCount", len(applyPolicyViolations), + "violations", applyPolicyViolations) + var applyPolicyFormatter func(report string) string + summary := fmt.Sprintf("Policy violation for <b>%v - %v</b>", job.ProjectName, command) + if reporter.SupportsMarkdown() { + applyPolicyFormatter = reporting.AsCollapsibleComment(summary, false) + } else { + applyPolicyFormatter = reporting.AsComment(summary) + } + + applyPolicyReportMessage := "Terraform apply failed validation checks :x:<br>" + if len(applyPolicyViolations) > 0 { + preformattedMessages := make([]string, 0) + for _, message := range applyPolicyViolations { + preformattedMessages = append(preformattedMessages, fmt.Sprintf(" %v", message)) + } + applyPolicyReportMessage = applyPolicyReportMessage + strings.Join(preformattedMessages, "<br>") + } + _, _, err = reporter.Report(applyPolicyReportMessage, applyPolicyFormatter) + if err != nil { + slog.Error("Failed to report apply policy violation.", "error", err) + } + + msg := fmt.Sprintf("Apply is not allowed due to policy violations") + slog.Error(msg) + return nil, msg, errors.New(msg) + } // Running apply diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/ee/cli/pkg/policy/policy.go new/digger-cli-0.6.140/ee/cli/pkg/policy/policy.go --- old/digger-cli-0.6.139/ee/cli/pkg/policy/policy.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/ee/cli/pkg/policy/policy.go 2026-01-16 00:22:27.000000000 +0100 @@ -3,6 +3,7 @@ import ( "github.com/diggerhq/digger/libs/git_utils" "github.com/samber/lo" + "log/slog" "os" "path" "path/filepath" @@ -115,6 +116,25 @@ return policy, nil } +func (p DiggerRepoPolicyProvider) GetApplyPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) { + slog.Debug("GetApplyPolicy called", + "organisation", organisation, + "repository", repository, + "projectname", projectname, + "projectDir", projectDir, + "managementRepoUrl", p.ManagementRepoUrl) + policy, err := p.getPolicyFileContents(repository, projectname, projectDir, "apply.rego") + if err != nil { + slog.Error("Error getting apply policy file", "error", err) + return policy, err + } + slog.Debug("GetApplyPolicy result", + "policyLength", len(policy), + "policyEmpty", policy == "", + "policyContent", policy) + return policy, nil +} + func (p DiggerRepoPolicyProvider) GetDriftPolicy() (string, error) { return "", nil diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/libs/policy/core.go new/digger-cli-0.6.140/libs/policy/core.go --- old/digger-cli-0.6.139/libs/policy/core.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/libs/policy/core.go 2026-01-16 00:22:27.000000000 +0100 @@ -4,6 +4,7 @@ GetAccessPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) GetPlanPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) GetDriftPolicy() (string, error) + GetApplyPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) GetOrganisation() string // TODO: remove this method from here since out of place } @@ -12,6 +13,7 @@ CheckAccessPolicy(SCMOrganisation string, SCMrepository string, projectName string, projectDir string, command string, prNumber *int, requestedBy string, teams []string, approvals []string, approvalTeams []string, planPolicyViolations []string) (bool, error) CheckPlanPolicy(SCMrepository string, SCMOrganisation string, projectname string, projectDir string, requestedBy string, teams []string, approvals []string, approvalTeams []string, planOutput string) (bool, []string, error) CheckDriftPolicy(SCMOrganisation string, SCMrepository string, projectname string) (bool, error) + CheckApplyPolicy(SCMOrganisation string, SCMrepository string, projectName string, projectDir string, command string, prNumber *int, requestedBy string, teams []string, approvals []string, approvalTeams []string, planPolicyViolations []string, planOutput string) (bool, []string, error) } type PolicyCheckerProvider interface { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/libs/policy/mocks.go new/digger-cli-0.6.140/libs/policy/mocks.go --- old/digger-cli-0.6.139/libs/policy/mocks.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/libs/policy/mocks.go 2026-01-16 00:22:27.000000000 +0100 @@ -14,3 +14,7 @@ func (t MockPolicyChecker) CheckDriftPolicy(SCMOrganisation string, SCMrepository string, projectname string) (bool, error) { return true, nil } + +func (t MockPolicyChecker) CheckApplyPolicy(SCMOrganisation string, SCMrepository string, projectName string, projectDir string, command string, prNumber *int, requestedBy string, teams []string, approvals []string, approvalTeams []string, planPolicyViolations []string, planOutput string) (bool, []string, error) { + return true, nil, nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/libs/policy/policy.go new/digger-cli-0.6.140/libs/policy/policy.go --- old/digger-cli-0.6.139/libs/policy/policy.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/libs/policy/policy.go 2026-01-16 00:22:27.000000000 +0100 @@ -42,6 +42,10 @@ return true, nil } +func (p NoOpPolicyChecker) CheckApplyPolicy(SCMOrganisation string, SCMrepository string, projectName string, projectDir string, command string, prNumber *int, requestedBy string, teams []string, approvals []string, approvalTeams []string, planPolicyViolations []string, planOutput string) (bool, []string, error) { + return true, nil, nil +} + func getAccessPolicyForOrganisation(p *DiggerHttpPolicyProvider) (string, *http.Response, error) { organisation := p.DiggerOrganisation u, err := url.Parse(p.DiggerHost) @@ -132,6 +136,36 @@ return string(body), resp, nil } +func getApplyPolicyForOrganisation(p *DiggerHttpPolicyProvider) (string, *http.Response, error) { + organisation := p.DiggerOrganisation + u, err := url.Parse(p.DiggerHost) + if err != nil { + slog.Error("Failed to parse digger cloud URL", "url", p.DiggerHost, "error", err) + return "", nil, fmt.Errorf("not able to parse digger cloud url: %v", err) + } + u.Path = "/orgs/" + organisation + "/apply-policy" + + slog.Debug("Fetching org apply policy", "organisation", organisation, "url", u.String()) + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return "", nil, err + } + req.Header.Add("Authorization", "Bearer "+p.AuthToken) + + resp, err := p.HttpClient.Do(req) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", resp, nil + } + return string(body), resp, nil +} + func getAccessPolicyForNamespace(p *DiggerHttpPolicyProvider, namespace string, projectName string) (string, *http.Response, error) { // fetch RBAC policies for project from Digger API u, err := url.Parse(p.DiggerHost) @@ -199,6 +233,39 @@ return string(body), resp, nil } +func getApplyPolicyForNamespace(p *DiggerHttpPolicyProvider, namespace string, projectName string) (string, *http.Response, error) { + u, err := url.Parse(p.DiggerHost) + if err != nil { + slog.Error("Failed to parse digger cloud URL", "url", p.DiggerHost, "error", err) + return "", nil, fmt.Errorf("not able to parse digger cloud url: %v", err) + } + u.Path = "/repos/" + namespace + "/projects/" + projectName + "/apply-policy" + + slog.Debug("Fetching namespace apply policy", + "namespace", namespace, + "projectName", projectName, + "url", u.String()) + + req, err := http.NewRequest("GET", u.String(), nil) + + if err != nil { + return "", nil, err + } + req.Header.Add("Authorization", "Bearer "+p.AuthToken) + + resp, err := p.HttpClient.Do(req) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", resp, nil + } + return string(body), resp, nil +} + // GetPolicy fetches policy for particular project, if not found then it will fallback to org level policy func (p DiggerHttpPolicyProvider) GetAccessPolicy(organisation string, repo string, projectName string, projectDir string) (string, error) { namespace := fmt.Sprintf("%v-%v", organisation, repo) @@ -334,6 +401,61 @@ } } +func (p DiggerHttpPolicyProvider) GetApplyPolicy(organisation string, repo string, projectName string, projectDir string) (string, error) { + namespace := fmt.Sprintf("%v-%v", organisation, repo) + + slog.Debug("Getting apply policy", + "organisation", organisation, + "repo", repo, + "projectName", projectName, + "projectDir", projectDir) + + content, resp, err := getApplyPolicyForNamespace(&p, namespace, projectName) + if err != nil { + slog.Error("Failed to fetch apply policy for namespace", + "namespace", namespace, + "error", err) + return "", err + } + + // project policy found + if resp.StatusCode == 200 && content != "" { + slog.Debug("Found project apply policy", "namespace", namespace, "projectName", projectName) + return content, nil + } + + // check if project policy was empty or not found (retrieve org policy if so) + if (resp.StatusCode == 200 && content == "") || resp.StatusCode == 404 { + slog.Debug("Project apply policy not found, falling back to org policy", + "organisation", organisation) + + content, resp, err := getApplyPolicyForOrganisation(&p) + if err != nil { + slog.Error("Failed to fetch apply policy for organisation", + "organisation", organisation, + "error", err) + return "", err + } + if resp.StatusCode == 200 { + slog.Debug("Found organisation apply policy", "organisation", organisation) + return content, nil + } else if resp.StatusCode == 404 { + slog.Debug("Organisation apply policy not found", "organisation", organisation) + return "", nil + } else { + slog.Error("Unexpected response for organisation policy", + "statusCode", resp.StatusCode, + "response", content) + return "", errors.New(fmt.Sprintf("unexpected response while fetching organisation policy: %v, code %v", content, resp.StatusCode)) + } + } else { + slog.Error("Unexpected response for project policy", + "statusCode", resp.StatusCode, + "response", content) + return "", errors.New(fmt.Sprintf("unexpected response while fetching project policy: %v code %v", content, resp.StatusCode)) + } +} + func (p DiggerHttpPolicyProvider) GetOrganisation() string { return p.DiggerOrganisation } @@ -585,6 +707,133 @@ return true, nil } +func (p DiggerPolicyChecker) CheckApplyPolicy(SCMOrganisation string, SCMrepository string, projectName string, projectDir string, command string, prNumber *int, requestedBy string, teams []string, approvals []string, approvalTeams []string, planPolicyViolations []string, planOutput string) (bool, []string, error) { + slog.Debug("Checking apply policy", + "organisation", SCMOrganisation, + "repository", SCMrepository, + "project", projectName, + "command", command, + "requestedBy", requestedBy, + "hasPlanOutput", planOutput != "") + + policy, err := p.PolicyProvider.GetApplyPolicy(SCMOrganisation, SCMrepository, projectName, projectDir) + + if err != nil { + slog.Error("Error fetching apply policy", "error", err) + return false, nil, err + } + + input := map[string]interface{}{ + "user": requestedBy, + "organisation": SCMOrganisation, + "teams": teams, + "approvals": approvals, + "approval_teams": approvalTeams, + "planPolicyViolations": planPolicyViolations, + "action": command, + "project": projectName, + } + + // Include terraform plan JSON if available + if planOutput != "" { + slog.Debug("Parsing terraform plan for apply policy", "planLength", len(planOutput)) + var tfplan map[string]interface{} + err := json.Unmarshal([]byte(planOutput), &tfplan) + if err != nil { + slog.Error("Failed to parse terraform plan JSON for apply policy", "error", err) + // Don't fail the policy check, just proceed without plan data + } else { + input["terraform"] = tfplan + slog.Debug("Terraform plan included in apply policy input", + "hasResourceChanges", tfplan["resource_changes"] != nil) + } + } else { + slog.Debug("No terraform plan available for apply policy check") + } + + if policy == "" { + slog.Debug("No apply policy found, allowing action") + return true, nil, nil + } + + ctx := context.Background() + slog.Debug("Evaluating apply policy", + "input", input, + "policy", policy) + + query, err := rego.New( + rego.Query("data.digger.deny"), + rego.Module("digger", policy), + ).PrepareForEval(ctx) + + if err != nil { + slog.Error("Failed to prepare apply policy evaluation", "error", err) + return false, nil, err + } + + results, err := query.Eval(ctx, rego.EvalInput(input)) + slog.Debug("OPA evaluation completed", + "resultsCount", len(results), + "error", err) + + if len(results) == 0 || len(results[0].Expressions) == 0 { + slog.Error("No result found from apply policy evaluation") + return false, nil, fmt.Errorf("no result found") + } + + expressions := results[0].Expressions + slog.Debug("Processing expressions from OPA results", + "expressionCount", len(expressions)) + + decisionsResult := make([]string, 0) + for i, expression := range expressions { + slog.Debug("Processing expression", + "index", i, + "valueType", fmt.Sprintf("%T", expression.Value), + "value", expression.Value) + + decisions, ok := expression.Value.([]interface{}) + + if !ok { + slog.Error("Apply policy decision is not a slice of interfaces", + "actualType", fmt.Sprintf("%T", expression.Value)) + return false, nil, fmt.Errorf("decision is not a slice of interfaces") + } + + slog.Debug("Decisions array received", + "decisionsCount", len(decisions)) + + if len(decisions) > 0 { + for j, d := range decisions { + decisionStr := d.(string) + decisionsResult = append(decisionsResult, decisionStr) + slog.Info("Apply policy violation found", + "index", j, + "reason", decisionStr) + } + } + } + + slog.Debug("Final decisions result", + "totalViolations", len(decisionsResult), + "violations", decisionsResult) + + if len(decisionsResult) > 0 { + slog.Info("Apply policy check failed", + "violations", len(decisionsResult), + "organisation", SCMOrganisation, + "repository", SCMrepository, + "project", projectName) + return false, decisionsResult, nil + } + + slog.Info("Apply policy allowed action", + "user", requestedBy, + "action", command, + "project", projectName) + return true, []string{}, nil +} + func NewPolicyChecker(hostname string, organisationName string, authToken string) Checker { var policyChecker Checker if os.Getenv("NO_BACKEND") == "true" { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/digger-cli-0.6.139/libs/policy/policy_test.go new/digger-cli-0.6.140/libs/policy/policy_test.go --- old/digger-cli-0.6.139/libs/policy/policy_test.go 2026-01-06 00:41:37.000000000 +0100 +++ new/digger-cli-0.6.140/libs/policy/policy_test.go 2026-01-16 00:22:27.000000000 +0100 @@ -59,6 +59,10 @@ return "package digger\n", nil } +func (s *DiggerDefaultPolicyProvider) GetApplyPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) { + return "package digger\n", nil +} + func (s *DiggerDefaultPolicyProvider) GetOrganisation() string { return "ORGANISATIONDIGGER" } @@ -90,6 +94,10 @@ return "package digger\n", nil } +func (s *DiggerExamplePolicyProvider) GetApplyPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) { + return "package digger\n", nil +} + func (s *DiggerExamplePolicyProvider) GetOrganisation() string { return "ORGANISATIONDIGGER" } @@ -124,6 +132,10 @@ return "package digger\n", nil } +func (s *DiggerExamplePolicyProvider2) GetApplyPolicy(organisation string, repository string, projectname string, projectDir string) (string, error) { + return "package digger\n", nil +} + func (s *DiggerExamplePolicyProvider2) GetOrganisation() string { return "ORGANISATIONDIGGER" } ++++++ digger-cli.obsinfo ++++++ --- /var/tmp/diff_new_pack.c0I6TZ/_old 2026-01-17 14:55:08.560158950 +0100 +++ /var/tmp/diff_new_pack.c0I6TZ/_new 2026-01-17 14:55:08.572159452 +0100 @@ -1,5 +1,5 @@ name: digger-cli -version: 0.6.139 -mtime: 1767656497 -commit: 31d84f9498d8d54bb173d086c83d6c9af314d603 +version: 0.6.140 +mtime: 1768519347 +commit: 4121d16e2490ab91cf08b62ad90740902b20e628 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/digger-cli/vendor.tar.gz /work/SRC/openSUSE:Factory/.digger-cli.new.1928/vendor.tar.gz differ: char 32, line 2
