Skip to content
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
13 changes: 9 additions & 4 deletions cmd/deploy/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ var gitOpsPublishCmd = &cobra.Command{
return fmt.Errorf("publication returned no result")
}
result := *mutationResult.GitOpsPublish
cli.Info("Service snapshot %s", result.SnapshotRevision)
cli.Info("Signed commit %s", result.Commit)
cli.Info("Tree %s", result.Tree)
cli.Info("Pull request %s", result.PullRequest)
Expand All @@ -155,17 +156,20 @@ var gitOpsObserveCmd = &cobra.Command{
if err != nil {
return err
}
if gitOpsRevision != "" && gitOpsRevision != publication.SnapshotRevision {
return fmt.Errorf("requested revision %s differs from published service snapshot %s", gitOpsRevision, publication.SnapshotRevision)
}
plane, err := control.NewAt(workspace.Dir())
if err != nil {
return err
}
defer plane.Close()
result, err := plane.ObserveGitOps(ctx, &gitops.ObserveRequest{
Module: module.Name, Environment: gitOpsEnv, AppProject: gitOpsProject,
Applications: gitOpsApplications, Revision: gitOpsRevision,
Applications: gitOpsApplications, Revision: publication.SnapshotRevision,
Commit: publication.Commit, Tree: publication.Tree, RenderDigest: publication.RenderDigest,
Repository: publication.Repository, Path: publication.Path,
PullRequest: publication.PullRequest, Timeout: gitOpsTimeout,
PullRequest: publication.PullRequest, Local: gitOpsLocal, Timeout: gitOpsTimeout,
})
if err != nil {
return err
Expand Down Expand Up @@ -243,6 +247,7 @@ func printPublishPlan(plan *gitops.PublishPlan) {
cli.Info("Base %s@%s", plan.BaseBranch, plan.BaseRevision)
cli.Info("Promotion branch %s", plan.PromotionBranch)
cli.Info("Render digest %s", plan.RenderDigest)
cli.Info("Service snapshot %s", plan.SnapshotRevision)
cli.Info("Changed files:")
for _, path := range plan.Changed {
cli.Info(" %s", path)
Expand Down Expand Up @@ -287,11 +292,11 @@ func init() {
}
gitOpsObserveCmd.Flags().StringVar(&gitOpsProject, "app-project", "", "Selected Argo CD AppProject")
gitOpsObserveCmd.Flags().StringSliceVar(&gitOpsApplications, "application", nil, "Argo CD application to observe (repeatable)")
gitOpsObserveCmd.Flags().StringVar(&gitOpsRevision, "revision", "", "Exact reviewed Git revision Argo CD must reconcile")
gitOpsObserveCmd.Flags().StringVar(&gitOpsRevision, "revision", "", "Expected immutable service snapshot revision")
gitOpsObserveCmd.Flags().BoolVar(&gitOpsLocal, "local", false, "Observe a disposable local GitOps qualification")
gitOpsObserveCmd.Flags().DurationVar(&gitOpsTimeout, "timeout", 10*time.Minute, "Maximum time to wait for Synced and Healthy")
gitOpsRollbackCmd.Flags().StringVar(&gitOpsRollbackRevision, "to-revision", "", "Previously reviewed Git revision to re-promote")
_ = gitOpsObserveCmd.MarkFlagRequired("app-project")
_ = gitOpsObserveCmd.MarkFlagRequired("application")
_ = gitOpsObserveCmd.MarkFlagRequired("revision")
_ = gitOpsRollbackCmd.MarkFlagRequired("to-revision")
}
25 changes: 13 additions & 12 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,7 @@ codefly deploy gitops publish payments --env production
# After review and merge:
codefly deploy gitops observe payments --env production \
--app-project payments \
--application payments-api \
--revision <exact-merge-commit>
--application payments-api

# Recovery is another reviewed promotion, never a direct cluster mutation:
codefly deploy gitops rollback payments --env production \
Expand All @@ -210,17 +209,19 @@ gitops:
```

Render first writes to a temporary sibling, rejects unsafe or non-promotable
manifests, and installs only
`deployments/environments/<environment>/modules/<module>`. The installed
manifests, and installs the selected environment bootstrap and exact service
graph under `deployments/modules/<module>`. The installed
`.codefly-render.json` contains the sorted file inventory and aggregate digest.
Publish clones `workspace.gitops.repo-url`, stages only
`<workspace.gitops.path>/<environment>/modules/<module>`, prints a stable plan
and diff, then
uses a single-use prepared mutation to create a signed commit, push without
force, and open or update a pull request. Observe requires an approved, merged
pull request and verifies the published repository subtree digest, exact Argo
CD revision, source path, project authority, cluster identity, sync, operation,
and Healthy status before writing evidence under `.codefly/gitops/evidence/`.
Publish clones `workspace.gitops.repo-url`, commits and advertises the immutable
service snapshot under
`<workspace.gitops.path>/deployments/modules/<module>/services`, invokes the
module generator against that exact snapshot, then creates the signed
publication commit and opens or updates a pull request. Planning does not
advertise the snapshot or mutate the remote. Observe requires an
approved, merged pull request and verifies the publication digest, the
snapshot revision bound into every Application, exact service paths, project
authority, cluster identity, sync, operation, and Healthy status before writing
evidence under `.codefly/gitops/evidence/`.
Publishing requires configured Git commit signing and an authenticated `gh`
session; observation uses the active authenticated `argocd` context. Rollback
refuses a target revision unless a prior Healthy reviewed evidence receipt
Expand Down
18 changes: 18 additions & 0 deletions pkg/deployments/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ func TestVerifyLocalK3dTargetRejectsRemoteKindsBeforeInspectingKubeconfig(t *tes
}
}

func TestKubernetesOutputProfileReservesEphemeralForVerifiedLocalApply(t *testing.T) {
require.Equal(
t,
builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_PROMOTABLE_GITOPS_V1,
KubernetesOutputProfile(nil),
)
require.Equal(
t,
builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_PROMOTABLE_GITOPS_V1,
KubernetesOutputProfile(&RenderManager{}),
)
require.Equal(
t,
builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_EPHEMERAL_LOCAL_APPLY_V1,
KubernetesOutputProfile(&LocalApplyManager{}),
)
}

func TestVerifyLocalK3dTargetRejectsStaleCurrentContext(t *testing.T) {
harness := newKubernetesCommandHarness(t)
harness.writeSelected(kubeconfigDocument("eks-production", "eks-production", "production", "https://eks.example.com"))
Expand Down
36 changes: 32 additions & 4 deletions pkg/deployments/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ type Manager interface {
Handle(ctx context.Context, service *resources.Service, module *resources.Module, deploy *builderv0.DeploymentOutput) error
}

type DeploymentOutputRequirement interface {
RequiresDeploymentOutput() bool
}

func RequiresDeploymentOutput(manager Manager) bool {
requirement, ok := manager.(DeploymentOutputRequirement)
return ok && requirement.RequiresDeploymentOutput()
}

type RenderedTreeEvidence struct {
Module string
Service string
Expand Down Expand Up @@ -70,18 +79,37 @@ func (r *evidenceRecorder) renderedTrees() []RenderedTreeEvidence {
return trees
}

func GetKubernetesDeployment(ctx context.Context, dockerBuildContext *builderv0.DockerBuildContext, workspace *resources.Workspace, module *resources.Module, service *resources.Service, env *resources.Environment, namespace string) (*builderv0.Deployment, error) {
func GetKubernetesDeployment(
ctx context.Context,
dockerBuildContext *builderv0.DockerBuildContext,
workspace *resources.Workspace,
module *resources.Module,
service *resources.Service,
namespace string,
profile builderv0.KubernetesOutputProfile,
secretReferences map[string]*builderv0.KubernetesSecretKeyReference,
) (*builderv0.Deployment, error) {
return &builderv0.Deployment{
Kind: &builderv0.Deployment_Kubernetes{
Kubernetes: &builderv0.KubernetesDeployment{
BuildContext: dockerBuildContext,
Namespace: namespace,
Destination: KustomizeDir(ctx, workspace, module, service),
BuildContext: dockerBuildContext,
Namespace: namespace,
Destination: KustomizeDir(ctx, workspace, module, service),
Profile: profile,
SecretReferences: secretReferences,
ValidateServerSide: profile == builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_PROMOTABLE_GITOPS_V1,
},
},
}, nil
}

func KubernetesOutputProfile(manager Manager) builderv0.KubernetesOutputProfile {
if _, directLocalApply := manager.(*LocalApplyManager); directLocalApply {
return builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_EPHEMERAL_LOCAL_APPLY_V1
}
return builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_PROMOTABLE_GITOPS_V1
}

func NewLocalApplyManager(ctx context.Context, workspace *resources.Workspace, env *resources.Environment) (*LocalApplyManager, error) {
target, err := VerifyLocalK3dTarget(ctx, env)
if err != nil {
Expand Down
Loading