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

appdev: support cnb versioning, clean up build env output #1215

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
75 changes: 40 additions & 35 deletions commands/apps_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ func RunAppsDevBuild(c *CmdConfig) error {

conf, err := newAppDevConfig(c)
if err != nil {
return err
return fmt.Errorf("initializing config: %w", err)
}

var spec *godo.AppSpec
var (
spec *godo.AppSpec
cnbVersioning *godo.AppBuildConfigCNBVersioning
)
appID, err := conf.GetString(doctl.ArgApp)
if err != nil {
return err
Expand All @@ -123,32 +126,30 @@ func RunAppsDevBuild(c *CmdConfig) error {
// TODO: if this is the user's first time running dev build, ask them if they'd like to
// link an existing app.
if appID != "" {
template.Print(`{{success checkmark}} fetching app details{{nl}}`, AppsDevDefaultSpecPath)
app, err := c.Apps().Get(appID)
if err != nil {
return err
}
spec = app.Spec
cnbVersioning = app.GetBuildConfig().GetCNBVersioning()
}

appSpecPath, err := conf.GetString(doctl.ArgAppSpec)
if err != nil {
return err
}

if spec == nil {
if appSpecPath == "" {
if _, err := os.Stat(AppsDevDefaultSpecPath); err == nil {
appSpecPath = AppsDevDefaultSpecPath
template.Print(heredoc.Doc(`
{{success checkmark}} using app spec at {{highlight .}}{{nl}}`,
), AppsDevDefaultSpecPath)
}
if spec == nil && appSpecPath == "" {
if _, err := os.Stat(AppsDevDefaultSpecPath); err == nil {
appSpecPath = AppsDevDefaultSpecPath
template.Print(`{{success checkmark}} using app spec at {{highlight .}}{{nl}}`, AppsDevDefaultSpecPath)
}
if appSpecPath != "" {
spec, err = readAppSpec(os.Stdin, appSpecPath)
if err != nil {
return err
}
}
if appSpecPath != "" {
spec, err = readAppSpec(os.Stdin, appSpecPath)
if err != nil {
return err
}
}

Expand All @@ -170,26 +171,31 @@ func RunAppsDevBuild(c *CmdConfig) error {
if len(c.Args) >= 1 {
component = c.Args[0]
}
if Interactive && component == "" {
if component == "" {
var components []list.Item
_ = godo.ForEachAppSpecComponent(spec, func(c godo.AppBuildableComponentSpec) error {
components = append(components, componentListItem{c})
return nil
})
list := list.New(components)
list.Model().Title = "select a component"
list.Model().SetStatusBarItemName("component", "components")
selected, err := list.Select()
if err != nil {
return err
} else if selected == nil {
return fmt.Errorf("cancelled")
}
selectedComponent, ok := selected.(componentListItem)
if !ok {
return fmt.Errorf("unexpected item type %T", selectedComponent)

if len(components) == 1 {
component = components[0].(componentListItem).spec.GetName()
} else if len(components) > 1 && Interactive {
list := list.New(components)
list.Model().Title = "select a component"
list.Model().SetStatusBarItemName("component", "components")
selected, err := list.Select()
if err != nil {
return err
} else if selected == nil {
return fmt.Errorf("cancelled")
}
selectedComponent, ok := selected.(componentListItem)
if !ok {
return fmt.Errorf("unexpected item type %T", selectedComponent)
}
component = selectedComponent.spec.GetName()
}
component = selectedComponent.spec.GetName()
}

if component == "" {
Expand All @@ -204,9 +210,10 @@ func RunAppsDevBuild(c *CmdConfig) error {
return err
}

buildingComponentLine := template.String(heredoc.Doc(`
building {{lower (snakeToTitle .GetType)}} {{highlight .GetName}}`,
), componentSpec)
buildingComponentLine := template.String(
`building {{lower (snakeToTitle .GetType)}} {{highlight .GetName}}`,
componentSpec,
)
template.Print(`{{success checkmark}} {{.}}{{nl 2}}`, buildingComponentLine)

if componentSpec.GetSourceDir() != "" {
Expand Down Expand Up @@ -239,9 +246,6 @@ func RunAppsDevBuild(c *CmdConfig) error {
if err != nil {
return err
}
if registryName == "" {
return errors.New("registry-name is required")
}

buildOverrride, err := conf.GetString(doctl.ArgAppDevBuildCommand)
if err != nil {
Expand Down Expand Up @@ -305,6 +309,7 @@ func RunAppsDevBuild(c *CmdConfig) error {
EnvOverride: envs,
BuildCommandOverride: buildOverrride,
LogWriter: logWriter,
Versioning: builder.Versioning{CNB: cnbVersioning},
})
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions commands/apps_dev_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func RunAppsDevConfigSet(c *CmdConfig) error {
}

for _, arg := range c.Args {
split := strings.Split(arg, "=")
split := strings.SplitN(arg, "=", 2)
if len(split) != 2 {
return errors.New("unexpected arg: " + arg)
}
Expand Down Expand Up @@ -223,7 +223,7 @@ func newAppDevConfig(cmdConfig *CmdConfig) (*appDevConfig, error) {
if err := ensureStringInFile(devConfigFilePath, ""); err != nil {
return nil, err
}
if err := ensureStringInFile(filepath.Join(configDir, ".gitignore"), "dev-config.yaml"); err != nil {
if err := ensureStringInFile(filepath.Join(configDir, ".gitignore"), DefaultDevConfigFile); err != nil {
return nil, err
}
} else if _, err := os.Stat(devConfigFilePath); err != nil {
Expand Down
4 changes: 3 additions & 1 deletion commands/charm/pager/pager.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ func WithTitle(title string) Option {

func (p *Pager) Write(b []byte) (int, error) {
n, err := p.buffer.Write(b)
p.prog.Send(msgUpdate{})
if p.prog != nil {
p.prog.Send(msgUpdate{})
}
return n, err
}

Expand Down
4 changes: 4 additions & 0 deletions commands/charm/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func Indent(level uint) io.Writer {
return indent.NewWriterPipe(os.Stdout, level, nil)
}

func IndentWriter(w io.Writer, level uint) io.Writer {
return indent.NewWriterPipe(w, level, nil)
}

func IndentString(level uint, str string) string {
return indent.String(str, level)
}
Expand Down
70 changes: 38 additions & 32 deletions internal/apps/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"os"
"time"

"github.com/MakeNowJust/heredoc"
"github.com/digitalocean/doctl/commands/charm"
"github.com/digitalocean/doctl/commands/charm/template"
"github.com/digitalocean/godo"
)
Expand Down Expand Up @@ -46,7 +46,12 @@ type baseComponentBuilder struct {
}

func (b baseComponentBuilder) ImageOutputName() string {
return fmt.Sprintf("%s/%s:dev", b.registry, b.component.GetName())
ref := fmt.Sprintf("%s:dev", b.component.GetName())
if b.registry != "" {
ref = fmt.Sprintf("%s/%s", b.registry, ref)
}

return ref
}

func (b baseComponentBuilder) getLogWriter() io.Writer {
Expand All @@ -56,55 +61,42 @@ func (b baseComponentBuilder) getLogWriter() io.Writer {
return b.logWriter
}

func (b baseComponentBuilder) getEnvMap() map[string]string {
envs := map[string]string{}
func (b baseComponentBuilder) getEnvMap() (map[string]string, error) {
envMap := map[string]string{}
lw := b.getLogWriter()
template.Render(lw, `{{success checkmark}} configuring build environment variables{{nl}}`, nil)
subLW := charm.IndentWriter(lw, 2)

template.Render(lw, heredoc.Doc(`
{{success checkmark}} configuring build environment variables... {{nl 2}}`,
), nil)

if b.spec != nil {
for _, e := range b.spec.Envs {
addEnvs := func(envs ...*godo.AppVariableDefinition) {
for _, e := range envs {
if e.Type == godo.AppVariableType_Secret {
template.Render(lw, heredoc.Doc(`
=> Ignoring SECRET variable {{highlight .GetKey}}{{nl}}`,
), e)
template.Render(subLW, `{{success checkmark}} ignoring SECRET variable {{highlight .GetKey}}{{nl}}`, e)
continue
}
if e.Scope != godo.AppVariableScope_RunTime {
val := e.Value
envs[e.Key] = val
envMap[e.Key] = val
}
}
}

for _, e := range b.component.GetEnvs() {
if e.Type == godo.AppVariableType_Secret {
template.Render(lw, heredoc.Doc(`
=> Ignoring SECRET variable {{highlight .GetKey}}{{nl}}`,
), e)
continue
}
if e.Scope != godo.AppVariableScope_RunTime {
val := e.Value
envs[e.Key] = val
}
}
addEnvs(b.spec.GetEnvs()...)
addEnvs(b.component.GetEnvs()...)

for k, v := range b.envOverrides {
v := v
if _, ok := envs[k]; ok {
template.Render(lw, heredoc.Doc(`
=> Overwriting {{highlight .}} with provided env value{{nl}}`,
), k)
_, exists := envMap[k]
if !exists {
// TODO: if interactive prompt to auto add to spec
return nil, fmt.Errorf("variable not in found in app spec: %s", k)
}
envs[k] = v
template.Render(subLW, `{{success checkmark}} overriding value for variable {{highlight .}}{{nl}}`, k)
envMap[k] = v
}

fmt.Fprint(lw, "\n")

return envs
return envMap, nil
}

// NewBuilderOpts ...
Expand All @@ -114,6 +106,11 @@ type NewBuilderOpts struct {
EnvOverride map[string]string
BuildCommandOverride string
LogWriter io.Writer
Versioning Versioning
}

type Versioning struct {
CNB *godo.AppBuildConfigCNBVersioning
}

// DefaultComponentBuilderFactory is the standard component builder factory.
Expand Down Expand Up @@ -141,6 +138,14 @@ func (f *DefaultComponentBuilderFactory) NewComponentBuilder(cli DockerEngineCli
copyOnWriteSemantics := true

if component.GetDockerfilePath() == "" {
var cnbVersioning CNBVersioning
for _, bp := range opts.Versioning.CNB.GetBuildpacks() {
cnbVersioning.Buildpacks = append(cnbVersioning.Buildpacks, &Buildpack{
ID: bp.ID,
Version: fmt.Sprintf("%d.0.0", bp.MajorVersion),
})
}

return &CNBComponentBuilder{
baseComponentBuilder: baseComponentBuilder{
cli,
Expand All @@ -153,6 +158,7 @@ func (f *DefaultComponentBuilderFactory) NewComponentBuilder(cli DockerEngineCli
copyOnWriteSemantics,
opts.LogWriter,
},
versioning: cnbVersioning,
}, nil
}

Expand Down
57 changes: 52 additions & 5 deletions internal/apps/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,24 @@ func TestNewBuilderComponent(t *testing.T) {
require.ErrorContains(t, err, fmt.Sprintf("component %s not found", missingComponent))
})

t.Run("cnb builder", func(t *testing.T) {
t.Run("dockerfile builder", func(t *testing.T) {
builder, err := builderFactory.NewComponentBuilder(nil, ".", &godo.AppSpec{
Services: []*godo.AppServiceSpec{{
Name: "web",
DockerfilePath: ".",
}},
}, NewBuilderOpts{
Component: "web",
})
require.NoError(t, err)
require.IsTypef(t, &DockerComponentBuilder{}, builder, "expected DockerComponentBuilder but was %T", builder)
})
}

func TestNewBuilderComponent_CNB(t *testing.T) {
builderFactory := DefaultComponentBuilderFactory{}

t.Run("happy path", func(t *testing.T) {
builder, err := builderFactory.NewComponentBuilder(nil, ".", &godo.AppSpec{
Services: []*godo.AppServiceSpec{{
Name: "web",
Expand All @@ -44,18 +61,48 @@ func TestNewBuilderComponent(t *testing.T) {
})
require.NoError(t, err)
require.IsTypef(t, &CNBComponentBuilder{}, builder, "expected CNBComponentBuilder but was %T", builder)

cnbBuilder := builder.(*CNBComponentBuilder)
// no buildpacks in builder opts
require.Equal(t, CNBVersioning{}, cnbBuilder.versioning)
})

t.Run("dockerfile builder", func(t *testing.T) {
t.Run("buildpack versioning", func(t *testing.T) {
builder, err := builderFactory.NewComponentBuilder(nil, ".", &godo.AppSpec{
Services: []*godo.AppServiceSpec{{
Name: "web",
DockerfilePath: ".",
Name: "web",
}},
}, NewBuilderOpts{
Component: "web",
Versioning: Versioning{
CNB: &godo.AppBuildConfigCNBVersioning{
Buildpacks: []*godo.Buildpack{
{
ID: "digitalocean/node",
MajorVersion: 1,
},
{
ID: "digitalocean/go",
MajorVersion: 2,
},
},
},
},
})
require.NoError(t, err)
require.IsTypef(t, &DockerComponentBuilder{}, builder, "expected DockerComponentBuilder but was %T", builder)
cnbBuilder, ok := builder.(*CNBComponentBuilder)
require.True(t, ok, "expected CNBComponentBuilder but was %T", builder)
require.Equal(t, CNBVersioning{
Buildpacks: []*Buildpack{
{
ID: "digitalocean/node",
Version: "1.0.0",
},
{
ID: "digitalocean/go",
Version: "2.0.0",
},
},
}, cnbBuilder.versioning)
})
}
Loading