diff --git a/cmd/experiment/experiment.go b/cmd/experiment/experiment.go index 1e0c024..cec7725 100644 --- a/cmd/experiment/experiment.go +++ b/cmd/experiment/experiment.go @@ -7,9 +7,9 @@ import ( "time" "github.com/get-glu/glu" - "github.com/get-glu/glu/pkg/controllers" "github.com/get-glu/glu/pkg/core" "github.com/get-glu/glu/pkg/fs" + "github.com/get-glu/glu/pkg/phases" "github.com/get-glu/glu/pkg/src/git" "github.com/get-glu/glu/pkg/src/oci" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -39,23 +39,23 @@ func run(ctx context.Context) error { // create initial (empty) pipeline pipeline := glu.NewPipeline(glu.Name("checkout"), NewCheckoutResource) - // build a controller which sources from the OCI repository - ociController, err := controllers.New(glu.Name("oci"), pipeline, ociSource) + // build a phase which sources from the OCI repository + ociPhase, err := phases.New(glu.Name("oci"), pipeline, ociSource) if err != nil { return nil, err } - // build a controller for the staging environment which source from the git repository - // configure it to promote from the OCI controller - staging, err := controllers.New(glu.Name("staging", glu.Label("env", "staging")), - pipeline, gitSource, core.PromotesFrom(ociController)) + // build a phase for the staging environment which source from the git repository + // configure it to promote from the OCI phase + staging, err := phases.New(glu.Name("staging", glu.Label("env", "staging")), + pipeline, gitSource, core.PromotesFrom(ociPhase)) if err != nil { return nil, err } - // build a controller for the production environment which source from the git repository - // configure it to promote from the staging git controller - _, err = controllers.New(glu.Name("production", glu.Label("env", "production")), + // build a phase for the production environment which source from the git repository + // configure it to promote from the staging git phase + _, err = phases.New(glu.Name("production", glu.Label("env", "production")), pipeline, gitSource, core.PromotesFrom(staging)) if err != nil { return nil, err @@ -66,8 +66,8 @@ func run(ctx context.Context) error { }).SchedulePromotion( glu.ScheduleInterval(10*time.Second), glu.ScheduleMatchesLabel("env", "staging"), - // alternatively, the controller instance can be target directly with: - // glu.ScheduleMatchesController(gitStaging), + // alternatively, the phase instance can be target directly with: + // glu.ScheduleMatchesPhase(gitStaging), ).Run() } @@ -87,26 +87,26 @@ func NewCheckoutResource() *CheckoutResource { // It should return a unique digest for the state of the resource. // In this instance we happen to be reading a unique digest from the source // and so we can lean into that. -// This will be used for comparisons in the controller to decided whether or not +// This will be used for comparisons in the phase to decided whether or not // a change has occurred when deciding if to update the target source. func (c *CheckoutResource) Digest() (string, error) { return c.ImageDigest, nil } // CommitMessage is an optional git specific method for overriding generated commit messages. -// The function is provided with the source controllers metadata and the previous value of resource. +// The function is provided with the source phases metadata and the previous value of resource. func (c *CheckoutResource) CommitMessage(meta glu.Metadata, _ *CheckoutResource) (string, error) { return fmt.Sprintf("feat: update app %q in %q", meta.Name, meta.Labels["env"]), nil } // ProposalTitle is an optional git specific method for overriding generated proposal message (PR/MR) title message. -// The function is provided with the source controllers metadata and the previous value of resource. +// The function is provided with the source phases metadata and the previous value of resource. func (c *CheckoutResource) ProposalTitle(meta glu.Metadata, r *CheckoutResource) (string, error) { return c.CommitMessage(meta, r) } // ProposalBody is an optional git specific method for overriding generated proposal body (PR/MR) body message. -// The function is provided with the source controllers metadata and the previous value of resource. +// The function is provided with the source phases metadata and the previous value of resource. func (c *CheckoutResource) ProposalBody(meta glu.Metadata, r *CheckoutResource) (string, error) { return fmt.Sprintf(`| app | from | to | | -------- | ---- | -- | @@ -125,9 +125,9 @@ func (c *CheckoutResource) ReadFromOCIDescriptor(d v1.Descriptor) error { // ReadFrom is a Git specific resource requirement. // It specifies how to read the resource from a target Filesystem. // The type should navigate and source the relevant state from the fileystem provided. -// The function is also provided with metadata for the calling controller. -// This allows the defining type to adjust behaviour based on the context of the controller. -// Here we are reading a yaml file from a directory signified by a label ("env") on the controller metadata. +// The function is also provided with metadata for the calling phase. +// This allows the defining type to adjust behaviour based on the context of the phase. +// Here we are reading a yaml file from a directory signified by a label ("env") on the phase metadata. func (c *CheckoutResource) ReadFrom(_ context.Context, meta core.Metadata, fs fs.Filesystem) error { fi, err := fs.OpenFile( fmt.Sprintf("/env/%s/apps/checkout/deployment.yaml", meta.Labels["env"]), @@ -155,9 +155,9 @@ func (c *CheckoutResource) ReadFrom(_ context.Context, meta core.Metadata, fs fs // WriteTo is a Git specific resource requirement. // It specifies how to write the resource to a target Filesystem. // The type should navigate and encode the state of the resource to the target Filesystem. -// The function is also provided with metadata for the calling controller. -// This allows the defining type to adjust behaviour based on the context of the controller. -// Here we are writing a yaml file to a directory signified by a label ("env") on the controller metadata. +// The function is also provided with metadata for the calling phase. +// This allows the defining type to adjust behaviour based on the context of the phase. +// Here we are writing a yaml file to a directory signified by a label ("env") on the phase metadata. func (c *CheckoutResource) WriteTo(ctx context.Context, meta glu.Metadata, fs fs.Filesystem) error { fi, err := fs.OpenFile( fmt.Sprintf("/env/%s/apps/checkout/deployment.yaml", meta.Labels["env"]), diff --git a/cmd/mock/mock.go b/cmd/mock/mock.go index d1bf80e..f74fbf1 100644 --- a/cmd/mock/mock.go +++ b/cmd/mock/mock.go @@ -6,8 +6,8 @@ import ( "os" "github.com/get-glu/glu" - "github.com/get-glu/glu/pkg/controllers" "github.com/get-glu/glu/pkg/core" + "github.com/get-glu/glu/pkg/phases" ) // MockResource represents our mock resource state @@ -42,7 +42,7 @@ func run(ctx context.Context) error { // OCI phase ociSource := NewMockSource() - ociController, err := controllers.New( + ociPhase, err := phases.New( glu.Name("cloud-controller-oci", glu.Label("type", "oci"), glu.Label("status", "running"), glu.Label("version", "v1.2.3")), ccPipeline, ociSource, @@ -53,7 +53,7 @@ func run(ctx context.Context) error { // Staging phase stagingSource := NewMockSource() - stagingController, err := controllers.New( + stagingPhase, err := phases.New( glu.Name("cloud-controller-staging", glu.Label("type", "staging"), glu.Label("environment", "staging"), @@ -61,7 +61,7 @@ func run(ctx context.Context) error { ), ccPipeline, stagingSource, - core.PromotesFrom(ociController), + core.PromotesFrom(ociPhase), ) if err != nil { return nil, err @@ -69,7 +69,7 @@ func run(ctx context.Context) error { // Production phases prodEastSource := NewMockSource() - controllers.New( + phases.New( glu.Name("cloud-controller-production-east-1", glu.Label("type", "production"), glu.Label("environment", "production"), @@ -78,11 +78,11 @@ func run(ctx context.Context) error { ), ccPipeline, prodEastSource, - core.PromotesFrom(stagingController), + core.PromotesFrom(stagingPhase), ) prodWestSource := NewMockSource() - controllers.New( + phases.New( glu.Name("cloud-controller-production-west-1", glu.Label("type", "production"), glu.Label("environment", "production"), @@ -91,7 +91,7 @@ func run(ctx context.Context) error { ), ccPipeline, prodWestSource, - core.PromotesFrom(stagingController), + core.PromotesFrom(stagingPhase), ) return ccPipeline, nil @@ -103,7 +103,7 @@ func run(ctx context.Context) error { // OCI phase fdOciSource := NewMockSource() - fdOciController, err := controllers.New( + fdOciPhase, err := phases.New( glu.Name("frontdoor-oci", glu.Label("type", "oci"), glu.Label("version", "v2.0.1"), glu.Label("builder", "docker")), fdPipeline, fdOciSource, @@ -114,7 +114,7 @@ func run(ctx context.Context) error { // Staging phase fdStagingSource := NewMockSource() - fdStagingController, err := controllers.New( + fdStagingPhase, err := phases.New( glu.Name("frontdoor-staging", glu.Label("type", "staging"), glu.Label("environment", "staging"), @@ -122,7 +122,7 @@ func run(ctx context.Context) error { ), fdPipeline, fdStagingSource, - core.PromotesFrom(fdOciController), + core.PromotesFrom(fdOciPhase), ) if err != nil { return nil, err @@ -130,7 +130,7 @@ func run(ctx context.Context) error { // Production phase fdProdSource := NewMockSource() - controllers.New( + phases.New( glu.Name("frontdoor-production", glu.Label("type", "production"), glu.Label("environment", "production"), @@ -139,7 +139,7 @@ func run(ctx context.Context) error { ), fdPipeline, fdProdSource, - core.PromotesFrom(fdStagingController), + core.PromotesFrom(fdStagingPhase), ) return fdPipeline, nil diff --git a/docs/README.md b/docs/README.md index c7cfadb..73b11f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,30 +22,30 @@ They're also not immutable and we would love for your help in shaping them. Glu has an opinionated set of models and abstractions, which when combined, allow you to build consistent command-line and server processes for orchestrating the progression of applications and configuration across target environments. ```go -// build a controller which sources from our OCI repository -ociController, err := controllers.New(glu.Name("oci"), pipeline, ociSource) +// build a phase which sources from our OCI repository +ociPhase, err := phases.New(glu.Name("oci"), pipeline, ociSource) if err != nil { return nil, err } -// build a controller for the staging environment which sources from our git repository -// and configures it to promote from the OCI controller -staging, err := controllers.New(glu.Name("staging", glu.Label("env", "staging")), - pipeline, gitSource, core.PromotesFrom(ociController)) +// build a phase for the staging environment which sources from our git repository +// and configures it to promote from the OCI phase +staging, err := phases.New(glu.Name("staging", glu.Label("env", "staging")), + pipeline, gitSource, core.PromotesFrom(ociPhase)) if err != nil { return nil, err } -// build a controller for the production environment which also sources from our git repository -// and configures it to promote from the staging git controller -_, err = controllers.New(glu.Name("production", glu.Label("env", "production")), +// build a phase for the production environment which also sources from our git repository +// and configures it to promote from the staging git phase +_, err = phases.New(glu.Name("production", glu.Label("env", "production")), pipeline, gitSource, core.PromotesFrom(staging)) if err != nil { return nil, err } ``` -The Glu framework comprises of a set of abstractions for declaring the resources (your applications and configuration), update strategies (we call them controllers) and rules for progression (how and when to promote) within a pipeline. +The Glu framework comprises of a set of abstractions for declaring the resources (your applications and configuration), update strategies (we call them phases) and rules for progression (how and when to promote) within a pipeline. ### System @@ -74,7 +74,7 @@ Resources are the primary definition of _what_ is being represented in your pipe In Go, resources are represented as an interface for the author (that's you) to fill in the blanks: ```go -// Resource is an instance of a resource in a controller. +// Resource is an instance of a resource in a phase. // Primarilly, it exposes a Digest method used to produce // a hash digest of the instances current state. type Resource interface { @@ -100,39 +100,39 @@ func (s *SomeResource) Digest() (string, error) { return s.ImageDigest, nil } ### Pipelines -Pipelines carry your specific resources type across controller destinations. +Pipelines carry your specific resources type across phase destinations. They coordinate the edges in the workflow that is your promotion pipeline. They are also responsible for constructing new instances of your resource types. -These are used when fetching and updating controllers and sources during promotion. +These are used when fetching and updating phases and sources during promotion. ```go pipeline := glu.NewPipeline(glu.Name("mypipeline"), func() *SomeResource { // this is an opportunity to set any initial default values // for fields before the rest of the fields are populated - // from a target controller source + // from a target phase source return &SomeResource{ImageName: "someimagename"} }) ``` -### Controllers +### Phases -Controllers have the job of interfacing with both sources and other upstream controllers to manage the promotion lifecycle. +Phases have the job of interfacing with both sources and other upstream phases to manage the promotion lifecycle. They have metadata to uniquely identify themselves within the context of a pipeline. They're also bound to a particular source implementation, and are optional dependent on an upstream source of the _same resource type_. -When a controller attempts a promotion (`controller.Promote(ctx)`) the following occurs: +When a phase attempts a promotion (`phase.Promote(ctx)`) the following occurs: -1. If there is no upstream controller to promote from, then return (no op). -2. Get the current resource state from the target source based on controller metadata. -3. Get the current resource state from the upstream promotion target controller. +1. If there is no upstream phase to promote from, then return (no op). +2. Get the current resource state from the target source based on phase metadata. +3. Get the current resource state from the upstream promotion target phase. 4. If the resource from (2) is equal to that of (3) (based on comparing their digest), the return (no op). 5. Update the state of the source with the state of the upstream resource (3) (this is a promotion). ### Sources -Sources are the core engine for controllers to both view and update resources in a target external system. -While controllers handle the lifecycle of promotion, sources interface with your resource types and sources of truth to perform relevant transactions. +Sources are the core engine for phases to both view and update resources in a target external system. +While phases handle the lifecycle of promotion, sources interface with your resource types and sources of truth to perform relevant transactions. Currently, Glu has implementations for the following sources: @@ -142,24 +142,24 @@ Currently, Glu has implementations for the following sources: We look to add more in the not-so-distant future. However, these can also be implemented by hand via the following interfaces: ```go -// Controller is the core interface for resource sourcing and management. +// Phase is the core interface for resource sourcing and management. // These types can be registered on pipelines and can depend upon on another for promotion. -type Controller interface { +type Phase interface { Metadata() Metadata Get(context.Context) (any, error) Promote(context.Context) error } -// ResourceController is a Controller bound to a Resource type R. -type ResourceController[R Resource] interface { - Controller +// ResourcePhase is a Phase bound to a Resource type R. +type ResourcePhase[R Resource] interface { + Phase GetResource(context.Context) (R, error) } ``` ### Triggers -Schedule promotions to run automatically on an interval for controllers matching a specific set of labels: +Schedule promotions to run automatically on an interval for phases matching a specific set of labels: ```go // schedule promotion attempts to staging every 10 seconds diff --git a/glu.go b/glu.go index 82f77e8..62c6f2a 100644 --- a/glu.go +++ b/glu.go @@ -60,9 +60,9 @@ func NewPipeline[R core.Resource](meta Metadata, newFn func() R) *core.Pipeline[ type Pipeline interface { Metadata() Metadata - ControllerByName(string) (core.Controller, error) - Controllers(...containers.Option[core.ControllersOptions]) iter.Seq[core.Controller] - Dependencies() map[core.Controller]core.Controller + PhaseByName(string) (core.Phase, error) + Phases(...containers.Option[core.PhaseOptions]) iter.Seq[core.Phase] + Dependencies() map[core.Phase]core.Phase } type System struct { @@ -220,23 +220,23 @@ func (s *System) inspect(ctx context.Context, args ...string) (err error) { if len(args) == 1 { fmt.Fprintln(wr, "NAME\tDEPENDS_ON") deps := pipeline.Dependencies() - for controller := range pipeline.Controllers() { + for phase := range pipeline.Phases() { dependsName := "" - if depends, ok := deps[controller]; ok && depends != nil { + if depends, ok := deps[phase]; ok && depends != nil { dependsName = depends.Metadata().Name } - fmt.Fprintf(wr, "%s\t%s\n", controller.Metadata().Name, dependsName) + fmt.Fprintf(wr, "%s\t%s\n", phase.Metadata().Name, dependsName) } return nil } - controller, err := pipeline.ControllerByName(args[1]) + phase, err := pipeline.PhaseByName(args[1]) if err != nil { return err } - inst, err := controller.Get(ctx) + inst, err := phase.Get(ctx) if err != nil { return err } @@ -253,7 +253,7 @@ func (s *System) inspect(ctx context.Context, args ...string) (err error) { } fmt.Fprintln(wr) - meta := controller.Metadata() + meta := phase.Metadata() fmt.Fprintf(wr, "%s", meta.Name) for _, field := range extraFields { fmt.Fprintf(wr, "\t%s", field[1]) @@ -291,8 +291,8 @@ func (s *System) promote(ctx context.Context, args ...string) error { ) set := flag.NewFlagSet("promote", flag.ExitOnError) - set.Var(&labels, "label", "selector for filtering controllers (format key=value)") - set.BoolVar(&all, "all", false, "promote all controllers (ignores label filters)") + set.Var(&labels, "label", "selector for filtering phases (format key=value)") + set.BoolVar(&all, "all", false, "promote all phases (ignores label filters)") if err := set.Parse(args); err != nil { return err } @@ -304,11 +304,11 @@ func (s *System) promote(ctx context.Context, args ...string) error { if set.NArg() == 0 { if len(labels) == 0 && !all { - return errors.New("please pass --all if you want to promote all controllers") + return errors.New("please pass --all if you want to promote all phases") } for _, pipeline := range s.pipelines { - if err := promoteAllControllers(ctx, pipeline.Controllers()); err != nil { + if err := promoteAllPhases(ctx, pipeline.Phases()); err != nil { return err } } @@ -321,17 +321,17 @@ func (s *System) promote(ctx context.Context, args ...string) error { return err } - controllers := pipeline.Controllers(core.HasAllLabels(labels)) + phases := pipeline.Phases(core.HasAllLabels(labels)) if set.NArg() < 2 { - return promoteAllControllers(ctx, controllers) + return promoteAllPhases(ctx, phases) } - controller, err := pipeline.ControllerByName(set.Arg(1)) + phase, err := pipeline.PhaseByName(set.Arg(1)) if err != nil { return err } - if err := controller.Promote(ctx); err != nil { + if err := phase.Promote(ctx); err != nil { return err } @@ -340,12 +340,12 @@ func (s *System) promote(ctx context.Context, args ...string) error { type Schedule struct { interval time.Duration - options []containers.Option[core.ControllersOptions] + options []containers.Option[core.PhaseOptions] } -func promoteAllControllers(ctx context.Context, controllers iter.Seq[core.Controller]) error { - for controller := range controllers { - if err := controller.Promote(ctx); err != nil { +func promoteAllPhases(ctx context.Context, phases iter.Seq[core.Phase]) error { + for phase := range phases { + if err := phase.Promote(ctx); err != nil { return err } } @@ -371,9 +371,9 @@ func ScheduleInterval(d time.Duration) containers.Option[Schedule] { } } -func ScheduleMatchesController(c core.Controller) containers.Option[Schedule] { +func ScheduleMatchesPhase(c core.Phase) containers.Option[Schedule] { return func(s *Schedule) { - s.options = append(s.options, core.IsController(c)) + s.options = append(s.options, core.IsPhase(c)) } } @@ -397,9 +397,9 @@ func (s *System) run(ctx context.Context) error { return case <-ticker.C: for _, pipeline := range s.pipelines { - for controller := range pipeline.Controllers(sch.options...) { - if err := controller.Promote(ctx); err != nil { - slog.Error("reconciling resource", "name", controller.Metadata().Name, "error", err) + for phase := range pipeline.Phases(sch.options...) { + if err := phase.Promote(ctx); err != nil { + slog.Error("reconciling resource", "name", phase.Metadata().Name, "error", err) } } } diff --git a/go.mod b/go.mod index d013c2a..62aed9a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/cors v1.2.1 github.com/go-git/go-billy/v5 v5.6.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-github/v64 v64.0.0 diff --git a/go.sum b/go.sum index 332a6b1..01e3e2b 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= diff --git a/pkg/core/core.go b/pkg/core/core.go index 78b7c5c..5d4b649 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -10,7 +10,10 @@ import ( ) var ( - ErrNotFound = errors.New("not found") + // ErrNotFound is returned when a particular resource cannot be located + ErrNotFound = errors.New("not found") + // ErrAlreadyExists is returned when an attempt is made to create a resource + // which already exists ErrAlreadyExists = errors.New("already exists") ) @@ -21,21 +24,22 @@ type Metadata struct { Labels map[string]string } -// Controller is the core interface for resource sourcing and management. +// Phase is the core interface for resource sourcing and management. // These types can be registered on pipelines and can depend upon on another for promotion. -type Controller interface { +type Phase interface { Metadata() Metadata Get(context.Context) (any, error) Promote(context.Context) error } -// Resource is an instance of a resource in a controller. +// Resource is an instance of a resource in a phase. // Primarilly, it exposes a Digest method used to produce // a hash digest of the resource instances current state. type Resource interface { Digest() (string, error) } +// Pipeline is a collection of phases for a given resource type R. type Pipeline[R Resource] struct { meta Metadata newFn func() R @@ -43,10 +47,11 @@ type Pipeline[R Resource] struct { } type entry[R Resource] struct { - ResourceController[R] - promotesFrom ResourceController[R] + ResourcePhase[R] + promotesFrom ResourcePhase[R] } +// NewPipeline constructs and configures a new instance of Pipeline func NewPipeline[R Resource](meta Metadata, newFn func() R) *Pipeline[R] { return &Pipeline[R]{ meta: meta, @@ -55,35 +60,43 @@ func NewPipeline[R Resource](meta Metadata, newFn func() R) *Pipeline[R] { } } +// AddOptions are used to configure the addition of a ResourcePhase to a Pipeline type AddOptions[R Resource] struct { entry entry[R] } -func PromotesFrom[R Resource](c ResourceController[R]) containers.Option[AddOptions[R]] { +// PromotesFrom configures a dependent Phase to promote from for the Phase being added. +func PromotesFrom[R Resource](c ResourcePhase[R]) containers.Option[AddOptions[R]] { return func(ao *AddOptions[R]) { ao.entry.promotesFrom = c } } -type ResourceController[R Resource] interface { - Controller +// ResourcePhase is a Phase bound to a particular resource type R. +type ResourcePhase[R Resource] interface { + Phase GetResource(context.Context) (R, error) } +// New calls the functions underlying resource constructor function to get a +// new default instance of the resource. func (p *Pipeline[R]) New() R { return p.newFn() } +// Metadata returns the metadata assocated with the Pipelines (name and labels). func (p *Pipeline[R]) Metadata() Metadata { return p.meta } -func (p *Pipeline[R]) Add(r ResourceController[R], opts ...containers.Option[AddOptions[R]]) error { - add := AddOptions[R]{entry: entry[R]{ResourceController: r}} +// Add will add the provided resource phase to the pipeline along with configuring +// any dependent promotion source phases if configured to do so. +func (p *Pipeline[R]) Add(r ResourcePhase[R], opts ...containers.Option[AddOptions[R]]) error { + add := AddOptions[R]{entry: entry[R]{ResourcePhase: r}} containers.ApplyAll(&add, opts...) if _, existing := p.nodes[r.Metadata().Name]; existing { - return fmt.Errorf("controller %q: %w", r.Metadata().Name, ErrAlreadyExists) + return fmt.Errorf("phase %q: %w", r.Metadata().Name, ErrAlreadyExists) } p.nodes[r.Metadata().Name] = add.entry @@ -91,7 +104,8 @@ func (p *Pipeline[R]) Add(r ResourceController[R], opts ...containers.Option[Add return nil } -func (p *Pipeline[R]) PromotedFrom(c ResourceController[R]) (ResourceController[R], bool) { +// PromotedFrom returns the phase which c is configured to promote from (get dependent phase). +func (p *Pipeline[R]) PromotedFrom(c ResourcePhase[R]) (ResourcePhase[R], bool) { dep, ok := p.nodes[c.Metadata().Name] if !ok || dep.promotesFrom == nil { return nil, false @@ -100,19 +114,22 @@ func (p *Pipeline[R]) PromotedFrom(c ResourceController[R]) (ResourceController[ return dep.promotesFrom, true } -type ControllersOptions struct { - controller Controller - labels map[string]string +// PhaseOptions scopes a call to get phases from a pipeline. +type PhaseOptions struct { + phase Phase + labels map[string]string } -func IsController(c Controller) containers.Option[ControllersOptions] { - return func(co *ControllersOptions) { - co.controller = c +// IsPhase causes a call to Phases to list specifically the provided phase p. +func IsPhase(p Phase) containers.Option[PhaseOptions] { + return func(co *PhaseOptions) { + co.phase = p } } -func HasLabel(k, v string) containers.Option[ControllersOptions] { - return func(co *ControllersOptions) { +// HasLabel causes a call to Phases to list any phase with the matching label paid k and v. +func HasLabel(k, v string) containers.Option[PhaseOptions] { + return func(co *PhaseOptions) { if co.labels == nil { co.labels = map[string]string{} } @@ -121,53 +138,58 @@ func HasLabel(k, v string) containers.Option[ControllersOptions] { } } -func HasAllLabels(labels map[string]string) containers.Option[ControllersOptions] { - return func(co *ControllersOptions) { +// HasAllLabels causes a call to Phases to list any phase which mataches all the provided labels. +func HasAllLabels(labels map[string]string) containers.Option[PhaseOptions] { + return func(co *PhaseOptions) { co.labels = labels } } -func (p *Pipeline[R]) ControllerByName(name string) (Controller, error) { +// PhaseByName returns the Phase (if it exists) with a matching name. +func (p *Pipeline[R]) PhaseByName(name string) (Phase, error) { entry, ok := p.nodes[name] if !ok { - return nil, fmt.Errorf("controller %q: %w", name, ErrNotFound) + return nil, fmt.Errorf("phase %q: %w", name, ErrNotFound) } - return entry.ResourceController, nil + return entry.ResourcePhase, nil } -func (p *Pipeline[R]) Controllers(opts ...containers.Option[ControllersOptions]) iter.Seq[Controller] { - var options ControllersOptions +// Phases lists all phases in the pipeline with optional predicates. +func (p *Pipeline[R]) Phases(opts ...containers.Option[PhaseOptions]) iter.Seq[Phase] { + var options PhaseOptions containers.ApplyAll(&options, opts...) - return iter.Seq[Controller](func(yield func(Controller) bool) { + return iter.Seq[Phase](func(yield func(Phase) bool) { for _, entry := range p.nodes { - if options.controller != nil && entry.ResourceController != options.controller { + if options.phase != nil && entry.ResourcePhase != options.phase { continue } - if !hasAllLabels(entry.ResourceController, options.labels) { + if !hasAllLabels(entry.ResourcePhase, options.labels) { continue } - if !yield(entry.ResourceController) { + if !yield(entry.ResourcePhase) { break } } }) } -func (p *Pipeline[R]) Dependencies() map[Controller]Controller { - deps := map[Controller]Controller{} +// Dependencies returns a map of Phase to Phase. +// This map contains mappings of Phases to their dependent promotion source Phase (if configured). +func (p *Pipeline[R]) Dependencies() map[Phase]Phase { + deps := map[Phase]Phase{} for _, entry := range p.nodes { - deps[entry.ResourceController] = entry.promotesFrom + deps[entry.ResourcePhase] = entry.promotesFrom } return deps } -// hasAllLabels returns true if the provided controller has all the supplied labels -func hasAllLabels(c Controller, toFind map[string]string) bool { +// hasAllLabels returns true if the provided phase has all the supplied labels +func hasAllLabels(c Phase, toFind map[string]string) bool { labels := c.Metadata().Labels for k, v := range toFind { if found, ok := labels[k]; !ok || v != found { diff --git a/pkg/controllers/controller.go b/pkg/phases/phases.go similarity index 62% rename from pkg/controllers/controller.go rename to pkg/phases/phases.go index 43b4ea4..785980a 100644 --- a/pkg/controllers/controller.go +++ b/pkg/phases/phases.go @@ -1,4 +1,4 @@ -package controllers +package phases import ( "context" @@ -11,59 +11,59 @@ import ( // Source is an interface around storage for resources. type Source[R core.Resource] interface { - View(_ context.Context, pipeline, controller core.Metadata, _ R) error + View(_ context.Context, pipeline, phase core.Metadata, _ R) error } type UpdatableSource[R core.Resource] interface { Source[R] - Update(_ context.Context, pipeline, controller core.Metadata, from, to R) error + Update(_ context.Context, pipeline, phase core.Metadata, from, to R) error } -// Pipeline is a set of controller with promotion dependencies between one another. +// Pipeline is a set of phase with promotion dependencies between one another. type Pipeline[R core.Resource] interface { New() R Metadata() core.Metadata - Add(r core.ResourceController[R], opts ...containers.Option[core.AddOptions[R]]) error - PromotedFrom(core.ResourceController[R]) (core.ResourceController[R], bool) + Add(r core.ResourcePhase[R], opts ...containers.Option[core.AddOptions[R]]) error + PromotedFrom(core.ResourcePhase[R]) (core.ResourcePhase[R], bool) } -type Controller[R core.Resource] struct { +type Phase[R core.Resource] struct { logger *slog.Logger meta core.Metadata pipeline Pipeline[R] source Source[R] } -func New[R core.Resource](meta core.Metadata, pipeline Pipeline[R], repo Source[R], opts ...containers.Option[core.AddOptions[R]]) (*Controller[R], error) { +func New[R core.Resource](meta core.Metadata, pipeline Pipeline[R], repo Source[R], opts ...containers.Option[core.AddOptions[R]]) (*Phase[R], error) { logger := slog.With("name", meta.Name, "pipeline", pipeline.Metadata().Name) for k, v := range meta.Labels { logger = logger.With(k, v) } - controller := &Controller[R]{ + phase := &Phase[R]{ logger: logger, meta: meta, pipeline: pipeline, source: repo, } - if err := pipeline.Add(controller, opts...); err != nil { + if err := pipeline.Add(phase, opts...); err != nil { return nil, err } - return controller, nil + return phase, nil } -func (i *Controller[R]) Metadata() core.Metadata { +func (i *Phase[R]) Metadata() core.Metadata { return i.meta } -func (i *Controller[R]) Get(ctx context.Context) (any, error) { +func (i *Phase[R]) Get(ctx context.Context) (any, error) { return i.GetResource(ctx) } // GetResource returns the identified resource as its concrete pointer type. -func (i *Controller[R]) GetResource(ctx context.Context) (a R, err error) { +func (i *Phase[R]) GetResource(ctx context.Context) (a R, err error) { a = i.pipeline.New() if err := i.source.View(ctx, i.pipeline.Metadata(), i.meta, a); err != nil { return a, err @@ -72,11 +72,11 @@ func (i *Controller[R]) GetResource(ctx context.Context) (a R, err error) { return a, nil } -// Promote causes the controller to attempt a promotion from a dependent controller. -// If there is no promotion controller, this process is skipped. -// The controller fetches both its current resource state, and that of the promotion source controller. -// If the resources differ, then the controller updates its source to match the promoted version. -func (i *Controller[R]) Promote(ctx context.Context) (err error) { +// Promote causes the phase to attempt a promotion from a dependent phase. +// If there is no promotion phase, this process is skipped. +// The phase fetches both its current resource state, and that of the promotion source phase. +// If the resources differ, then the phase updates its source to match the promoted version. +func (i *Phase[R]) Promote(ctx context.Context) (err error) { i.logger.Debug("Promotion started") defer func() { i.logger.Debug("Promotion finished") diff --git a/pkg/src/git/source.go b/pkg/src/git/source.go index 5ba13e5..410e829 100644 --- a/pkg/src/git/source.go +++ b/pkg/src/git/source.go @@ -10,15 +10,15 @@ import ( "github.com/get-glu/glu/internal/git" "github.com/get-glu/glu/pkg/containers" - "github.com/get-glu/glu/pkg/controllers" "github.com/get-glu/glu/pkg/core" "github.com/get-glu/glu/pkg/fs" + "github.com/get-glu/glu/pkg/phases" "github.com/go-git/go-git/v5/plumbing" ) var ErrProposalNotFound = errors.New("proposal not found") -var _ controllers.Source[Resource] = (*Source[Resource])(nil) +var _ phases.Source[Resource] = (*Source[Resource])(nil) type Resource interface { core.Resource @@ -58,7 +58,7 @@ type ProposalOption struct { Labels []string } -// ProposeChanges configures the controller to propose the change (via PR or MR) +// ProposeChanges configures the phase to propose the change (via PR or MR) // as opposed to directly integrating it into the target trunk branch. func ProposeChanges[A Resource](opts ProposalOption) containers.Option[Source[A]] { return func(i *Source[A]) { @@ -82,7 +82,7 @@ type Branched interface { Branch() string } -func (g *Source[A]) View(ctx context.Context, _, controller core.Metadata, r A) error { +func (g *Source[A]) View(ctx context.Context, _, phase core.Metadata, r A) error { g.mu.RLock() defer g.mu.RUnlock() @@ -98,33 +98,33 @@ func (g *Source[A]) View(ctx context.Context, _, controller core.Metadata, r A) } return g.repo.View(ctx, func(hash plumbing.Hash, fs fs.Filesystem) error { - return r.ReadFrom(ctx, controller, fs) + return r.ReadFrom(ctx, phase, fs) }, opts...) } type commitMessage[A Resource] interface { // CommitMessage is an optional git specific method for overriding generated commit messages. - // The function is provided with the source controllers metadata and the previous value of resource. + // The function is provided with the source phases metadata and the previous value of resource. CommitMessage(meta core.Metadata, from A) (string, error) } type proposalTitle[A Resource] interface { // ProposalTitle is an optional git specific method for overriding generated proposal message (PR/MR) title message. - // The function is provided with the source controllers metadata and the previous value of resource. + // The function is provided with the source phases metadata and the previous value of resource. ProposalTitle(meta core.Metadata, from A) (string, error) } type proposalBody[A Resource] interface { // ProposalBody is an optional git specific method for overriding generated proposal body (PR/MR) body message. - // The function is provided with the source controllers metadata and the previous value of resource. + // The function is provided with the source phases metadata and the previous value of resource. ProposalBody(meta core.Metadata, from A) (string, error) } -func (g *Source[A]) Update(ctx context.Context, pipeline, controller core.Metadata, from, to A) error { +func (g *Source[A]) Update(ctx context.Context, pipeline, phase core.Metadata, from, to A) error { g.mu.Lock() defer g.mu.Unlock() - slog := slog.With("name", controller.Name) + slog := slog.With("name", phase.Name) // perform an initial fetch to ensure we're up to date // TODO(georgmac): scope to phase branch and proposal prefix @@ -133,16 +133,16 @@ func (g *Source[A]) Update(ctx context.Context, pipeline, controller core.Metada return fmt.Errorf("fetching upstream during update: %w", err) } - message := fmt.Sprintf("Update %s", controller.Name) + message := fmt.Sprintf("Update %s", phase.Name) if m, ok := core.Resource(to).(commitMessage[A]); ok { - message, err = m.CommitMessage(controller, from) + message, err = m.CommitMessage(phase, from) if err != nil { return fmt.Errorf("overriding commit message during update: %w", err) } } update := func(fs fs.Filesystem) (string, error) { - if err := to.WriteTo(ctx, controller, fs); err != nil { + if err := to.WriteTo(ctx, phase, fs); err != nil { return "", err } @@ -185,7 +185,7 @@ func (g *Source[A]) Update(ctx context.Context, pipeline, controller core.Metada // create branch name and check if this phase, resource and state has previously been observed var ( - branchPrefix = fmt.Sprintf("glu/%s/%s", pipeline.Name, controller.Name) + branchPrefix = fmt.Sprintf("glu/%s/%s", pipeline.Name, phase.Name) branch = path.Join(branchPrefix, digest) ) @@ -249,7 +249,7 @@ func (g *Source[A]) Update(ctx context.Context, pipeline, controller core.Metada title := message if p, ok := core.Resource(to).(proposalTitle[A]); ok { - title, err = p.ProposalTitle(controller, from) + title, err = p.ProposalTitle(phase, from) if err != nil { return err } @@ -260,7 +260,7 @@ func (g *Source[A]) Update(ctx context.Context, pipeline, controller core.Metada | %s | %s | `, fromDigest, digest) if b, ok := core.Resource(to).(proposalBody[A]); ok { - body, err = b.ProposalBody(controller, from) + body, err = b.ProposalBody(phase, from) if err != nil { return err } diff --git a/pkg/src/oci/oci.go b/pkg/src/oci/oci.go index 013fedb..5c4ae05 100644 --- a/pkg/src/oci/oci.go +++ b/pkg/src/oci/oci.go @@ -3,12 +3,12 @@ package oci import ( "context" - "github.com/get-glu/glu/pkg/controllers" "github.com/get-glu/glu/pkg/core" + "github.com/get-glu/glu/pkg/phases" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) -var _ controllers.Source[Resource] = (*Source[Resource])(nil) +var _ phases.Source[Resource] = (*Source[Resource])(nil) type Resource interface { core.Resource diff --git a/server.go b/server.go index 51d7b5d..0eef1de 100644 --- a/server.go +++ b/server.go @@ -10,6 +10,7 @@ import ( "github.com/get-glu/glu/pkg/core" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" ) type Server struct { @@ -34,6 +35,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) setupRoutes() { s.router.Use(middleware.Logger) s.router.Use(middleware.Recoverer) + s.router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"http://*", "https://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + MaxAge: 300, + })) s.router.Use(middleware.SetHeader("Content-Type", "application/json")) s.router.Use(middleware.StripSlashes) @@ -45,7 +53,7 @@ func (s *Server) setupRoutes() { s.router.Route("/api/v1", func(r chi.Router) { r.Get("/pipelines", s.listPipelines) r.Get("/pipelines/{pipeline}", s.getPipeline) - r.Get("/pipelines/{pipeline}/controllers/{controller}", s.getController) + r.Get("/pipelines/{pipeline}/phases/{phase}", s.getPhase) }) } @@ -56,12 +64,12 @@ type listPipelinesResponse struct { } type pipelineResponse struct { - Name string `json:"name"` - Labels map[string]string `json:"labels,omitempty"` - Controllers []controllerResponse `json:"controllers,omitempty"` + Name string `json:"name"` + Labels map[string]string `json:"labels,omitempty"` + Phases []phaseResponse `json:"phases,omitempty"` } -type controllerResponse struct { +type phaseResponse struct { Name string `json:"name"` DependsOn string `json:"depends_on,omitempty"` Labels map[string]string `json:"labels,omitempty"` @@ -77,24 +85,24 @@ func (s *Server) getPipelineByName(name string) (Pipeline, error) { return pipeline, nil } -func (s *Server) createControllerResponse(controller core.Controller, dependencies map[core.Controller]core.Controller) controllerResponse { +func (s *Server) createPhaseResponse(phase core.Phase, dependencies map[core.Phase]core.Phase) phaseResponse { var ( dependsOn string labels map[string]string ) if dependencies != nil { - if d, ok := dependencies[controller]; ok && d != nil { + if d, ok := dependencies[phase]; ok && d != nil { dependsOn = d.Metadata().Name } } - if controller.Metadata().Labels != nil { - labels = controller.Metadata().Labels + if phase.Metadata().Labels != nil { + labels = phase.Metadata().Labels } - return controllerResponse{ - Name: controller.Metadata().Name, + return phaseResponse{ + Name: phase.Metadata().Name, DependsOn: dependsOn, Labels: labels, } @@ -102,18 +110,18 @@ func (s *Server) createControllerResponse(controller core.Controller, dependenci func (s *Server) createPipelineResponse(ctx context.Context, pipeline Pipeline) (pipelineResponse, error) { dependencies := pipeline.Dependencies() - controllers := make([]controllerResponse, 0) + phases := make([]phaseResponse, 0) - for controller := range pipeline.Controllers() { - response := s.createControllerResponse(controller, dependencies) + for phase := range pipeline.Phases() { + response := s.createPhaseResponse(phase, dependencies) - v, err := controller.Get(ctx) + v, err := phase.Get(ctx) if err != nil { return pipelineResponse{}, err } response.Value = v - controllers = append(controllers, response) + phases = append(phases, response) } var labels map[string]string @@ -122,9 +130,9 @@ func (s *Server) createPipelineResponse(ctx context.Context, pipeline Pipeline) } return pipelineResponse{ - Name: pipeline.Metadata().Name, - Labels: labels, - Controllers: controllers, + Name: pipeline.Metadata().Name, + Labels: labels, + Phases: phases, }, nil } @@ -168,15 +176,15 @@ func (s *Server) getPipeline(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -func (s *Server) getController(w http.ResponseWriter, r *http.Request) { +func (s *Server) getPhase(w http.ResponseWriter, r *http.Request) { pipeline, err := s.getPipelineByName(chi.URLParam(r, "pipeline")) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } - controllerName := chi.URLParam(r, "controller") - controller, err := pipeline.ControllerByName(controllerName) + phaseName := chi.URLParam(r, "phase") + phase, err := pipeline.PhaseByName(phaseName) if err != nil { status := http.StatusInternalServerError if errors.Is(err, core.ErrNotFound) { @@ -187,13 +195,13 @@ func (s *Server) getController(w http.ResponseWriter, r *http.Request) { return } - v, err := controller.Get(r.Context()) + v, err := phase.Get(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - response := s.createControllerResponse(controller, pipeline.Dependencies()) + response := s.createPhaseResponse(phase, pipeline.Dependencies()) response.Value = v json.NewEncoder(w).Encode(response) diff --git a/ui/src/app/workflow.tsx b/ui/src/app/workflow.tsx index 0d0da15..960fa2d 100644 --- a/ui/src/app/workflow.tsx +++ b/ui/src/app/workflow.tsx @@ -6,14 +6,14 @@ import { ThemeToggle } from '@/components/theme-toggle'; import { useTheme } from '@/components/theme-provider'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { setFlow, updateNodes, updateEdges } from '@/store/flowSlice'; -import { ControllerNode } from '@/components/node'; +import { PhaseNode } from '@/components/node'; import { Pipeline } from '@/types/pipeline'; import { FlowPipeline, PipelineNode, PipelineEdge } from '@/types/flow'; import { GroupNode } from '@/components/group-node'; import { listPipelines } from '@/services/api'; const nodeTypes = { - controller: ControllerNode, + phase: PhaseNode, group: GroupNode }; @@ -83,29 +83,29 @@ export function transformPipelines(pipelines: Pipeline[]): FlowPipeline { const edges: PipelineEdge[] = []; const PIPELINE_VERTICAL_PADDING = 100; const PIPELINE_SPACING = 100; - const CONTROLLER_SPACING_X = 350; - const CONTROLLER_SPACING_Y = 200; + const PHASE_SPACING_X = 350; + const PHASE_SPACING_Y = 200; const NODE_WIDTH = 200; // Base width of a node let currentY = 0; pipelines.forEach((pipeline, pipelineIndex) => { - // First pass: calculate max depth and count controllers per column - const controllersByColumn: { [key: number]: number } = {}; + // First pass: calculate max depth and count phases per column + const phasesByColumn: { [key: number]: number } = {}; let maxDepth = 0; - pipeline.controllers.forEach((controller) => { - const depth = getNodeDepth(controller, pipeline.controllers); + pipeline.phases.forEach((phase) => { + const depth = getNodeDepth(phase, pipeline.phases); maxDepth = Math.max(maxDepth, depth); - const xPosition = depth * CONTROLLER_SPACING_X; - controllersByColumn[xPosition] = (controllersByColumn[xPosition] || 0) + 1; + const xPosition = depth * PHASE_SPACING_X; + phasesByColumn[xPosition] = (phasesByColumn[xPosition] || 0) + 1; }); - const maxPhasesInColumn = Math.max(...Object.values(controllersByColumn), 1); - const pipelineHeight = maxPhasesInColumn * CONTROLLER_SPACING_Y + PIPELINE_VERTICAL_PADDING; + const maxPhasesInColumn = Math.max(...Object.values(phasesByColumn), 1); + const pipelineHeight = maxPhasesInColumn * PHASE_SPACING_Y + PIPELINE_VERTICAL_PADDING; // Calculate required width based on deepest node plus padding - const requiredWidth = maxDepth * CONTROLLER_SPACING_X + NODE_WIDTH + 125; // 125px for padding + const requiredWidth = maxDepth * PHASE_SPACING_X + NODE_WIDTH + 125; // 125px for padding // Add group node const groupNode: PipelineNode = { @@ -122,29 +122,29 @@ export function transformPipelines(pipelines: Pipeline[]): FlowPipeline { const columnCount: { [key: number]: number } = {}; - pipeline.controllers.forEach((controller) => { - const xPosition = getNodeDepth(controller, pipeline.controllers) * CONTROLLER_SPACING_X + 50; + pipeline.phases.forEach((phase) => { + const xPosition = getNodeDepth(phase, pipeline.phases) * PHASE_SPACING_X + 50; columnCount[xPosition] = (columnCount[xPosition] || 0) + 1; const yPosition = - PIPELINE_VERTICAL_PADDING / 2 + (columnCount[xPosition] - 1) * CONTROLLER_SPACING_Y; + PIPELINE_VERTICAL_PADDING / 2 + (columnCount[xPosition] - 1) * PHASE_SPACING_Y; const node: PipelineNode = { - id: controller.name, - type: 'controller', + id: phase.name, + type: 'phase', position: { x: xPosition, y: yPosition }, parentId: pipeline.name, data: { - name: controller.name, - labels: controller.labels || {} + name: phase.name, + labels: phase.labels || {} } }; nodes.push(node); - if (controller.depends_on) { + if (phase.depends_on) { edges.push({ - id: `edge-${controller.depends_on}-${controller.name}`, - source: controller.depends_on, - target: controller.name + id: `edge-${phase.depends_on}-${phase.name}`, + source: phase.depends_on, + target: phase.name }); } }); @@ -160,15 +160,12 @@ export function transformPipelines(pipelines: Pipeline[]): FlowPipeline { return { nodes, edges }; } -// Helper function to calculate the depth of a controller based on its dependencies -function getNodeDepth( - controller: Pipeline['controllers'][0], - allControllers: Pipeline['controllers'] -): number { - if (!controller.depends_on) return 0; +// Helper function to calculate the depth of a phase based on its dependencies +function getNodeDepth(phase: Pipeline['phases'][0], allPhases: Pipeline['phases']): number { + if (!phase.depends_on) return 0; - const parentController = allControllers.find((c) => c.name === controller.depends_on); - if (!parentController) return 0; + const parentPhase = allPhases.find((c) => c.name === phase.depends_on); + if (!parentPhase) return 0; - return 1 + getNodeDepth(parentController, allControllers); + return 1 + getNodeDepth(parentPhase, allPhases); } diff --git a/ui/src/components/node.tsx b/ui/src/components/node.tsx index 5bcff2d..87e79b9 100644 --- a/ui/src/components/node.tsx +++ b/ui/src/components/node.tsx @@ -2,13 +2,13 @@ import { Handle, Position } from '@xyflow/react'; import { Package, GitBranch } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; -interface ControllerData { +interface PhaseData { name: string; type: string; labels?: Record; } -const ControllerNode = ({ data }: { data: ControllerData }) => { +const PhaseNode = ({ data }: { data: PhaseData }) => { const getIcon = () => { switch (data.type) { case 'oci': @@ -64,4 +64,4 @@ function getLabelColor(key: string, value: string): string { return colors[Math.abs(hash) % colors.length]; } -export { ControllerNode }; +export { PhaseNode }; diff --git a/ui/src/types/flow.ts b/ui/src/types/flow.ts index 4e7b758..e5e4143 100644 --- a/ui/src/types/flow.ts +++ b/ui/src/types/flow.ts @@ -6,7 +6,7 @@ export interface FlowPipeline { } export interface PipelineNode extends Node { - type: 'controller' | 'group'; + type: 'phase' | 'group'; data: any; parentNode?: string; } diff --git a/ui/src/types/pipeline.ts b/ui/src/types/pipeline.ts index dc4bd04..71c8a31 100644 --- a/ui/src/types/pipeline.ts +++ b/ui/src/types/pipeline.ts @@ -1,10 +1,10 @@ // Server-side pipeline types export interface Pipeline { name: string; - controllers: Controller[]; + phases: Phase[]; } -export interface Controller { +export interface Phase { name: string; depends_on?: string; labels?: Record;