diff --git a/build.sh b/build.sh index da5f79dbd..e50006f21 100755 --- a/build.sh +++ b/build.sh @@ -61,4 +61,8 @@ echo "Generating JsonSchemas" generator/build/generator "schemas" "output:schemas:artifacts:config=schemas" "paths=./pkg/apis/workspaces/v1alpha2" +echo "Generating Getter Implementations" + +generator/build/generator "getters" "paths=./pkg/apis/workspaces/v1alpha2" + echo "Finished generation of required GO sources, K8S CRDs, and Json Schemas" diff --git a/generator/getters/gen.go b/generator/getters/gen.go new file mode 100644 index 000000000..2c70b5d5f --- /dev/null +++ b/generator/getters/gen.go @@ -0,0 +1,134 @@ +package getters + +import ( + "bytes" + "fmt" + "github.com/devfile/api/generator/genutils" + "github.com/elliotchance/orderedmap" + "go/ast" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/markers" + "strconv" +) + +//go:generate go run sigs.k8s.io/controller-tools/cmd/helpgen generate:headerFile=../header.go.txt,year=2021 paths=. + +var ( + // GetterTypeMarker is associated with a type that's used as the pointer receiver of the getter method + GetterTypeMarker = markers.Must(markers.MakeDefinition("devfile:getter:generate", markers.DescribesType, struct{}{})) + // DefaultFieldMarker is associated with a boolean pointer field to indicate the default boolean value + DefaultFieldMarker = markers.Must(markers.MakeDefinition("devfile:default:value", markers.DescribesField, "")) +) + +// +controllertools:marker:generateHelp + +// Generator generates getter methods that are used to return values for the boolean pointer fields. +// +// The pointer receiver is determined from the `devfile:getter:generate` annotated type. The method will return the value of the +// field if it's been set, otherwise it will return the default value specified by the devfile:default:value annotation. +type Generator struct{} + +// RegisterMarkers registers the markers of the Generator +func (Generator) RegisterMarkers(into *markers.Registry) error { + if err := markers.RegisterAll(into, GetterTypeMarker, DefaultFieldMarker); err != nil { + return err + } + into.AddHelp(GetterTypeMarker, + markers.SimpleHelp("Devfile", "indicates the type that's used as the pointer receiver of the getter method")) + into.AddHelp(DefaultFieldMarker, + markers.SimpleHelp("Devfile", "indicates the default value of a boolean pointer field")) + return genutils.RegisterUnionMarkers(into) + +} + +func (Generator) CheckFilter() loader.NodeFilter { + return func(node ast.Node) bool { + // ignore interfaces + _, isIface := node.(*ast.InterfaceType) + return !isIface + } +} + +// getterInfo stores the info to generate the getter method +type getterInfo struct { + funcName string + defaultVal string +} + +// Generate generates the artifacts +func (g Generator) Generate(ctx *genall.GenerationContext) error { + for _, root := range ctx.Roots { + ctx.Checker.Check(root) + root.NeedTypesInfo() + + typesToProcess := orderedmap.NewOrderedMap() + if err := markers.EachType(ctx.Collector, root, func(info *markers.TypeInfo) { + if info.Markers.Get(GetterTypeMarker.Name) != nil { + var getters []getterInfo + for _, field := range info.Fields { + defaultVal := field.Markers.Get(DefaultFieldMarker.Name) + if defaultVal != nil { + if _, err := strconv.ParseBool(defaultVal.(string)); err != nil { + root.AddError(fmt.Errorf("devfile:default:value marker specified on %s/%s does not have a true or false value. Value is %s", info.Name, field.Name, defaultVal.(string))) + } + + //look for boolean pointers + if ptr, isPtr := field.RawField.Type.(*ast.StarExpr); isPtr { + if ident, ok := ptr.X.(*ast.Ident); ok { + if ident.Name == "bool" { + getters = append(getters, getterInfo{ + field.Name, + defaultVal.(string), + }) + } else { + root.AddError(fmt.Errorf("devfile:default:value marker is specified on %s/%s which is not a boolean pointer", info.Name, field.Name)) + } + } + } else { + root.AddError(fmt.Errorf("devfile:default:value marker is specified on %s/%s which is not a boolean pointer", info.Name, field.Name)) + } + + } + } + if len(getters) > 0 { + typesToProcess.Set(info, getters) + } else { + root.AddError(fmt.Errorf("type %s does not have the field marker, devfile:default:value specified on a boolean pointer field", info.Name)) + } + return + } + + }); err != nil { + root.AddError(err) + return nil + } + + genutils.WriteFormattedSourceFile("getters", ctx, root, func(buf *bytes.Buffer) { + for elt := typesToProcess.Front(); elt != nil; elt = elt.Next() { + cmd := elt.Key.(*markers.TypeInfo) + fields := elt.Value.([]getterInfo) + for _, getter := range fields { + fName := getter.funcName + defaultVal := getter.defaultVal + getterMethod := fmt.Sprintf(` +// Get%[1]s returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *%[2]s) Get%[1]s() bool { +return getBoolOrDefault(in.%[1]s, %[3]s)}`, fName, cmd.Name, defaultVal) + buf.WriteString(getterMethod) + } + } + + internalHelper := ` + +func getBoolOrDefault(input *bool, defaultVal bool) bool { + if input != nil { + return *input + } + return defaultVal }` + buf.WriteString(internalHelper) + }) + } + + return nil +} diff --git a/generator/getters/zz_generated.markerhelp.go b/generator/getters/zz_generated.markerhelp.go new file mode 100644 index 000000000..98f022020 --- /dev/null +++ b/generator/getters/zz_generated.markerhelp.go @@ -0,0 +1,22 @@ +// +build !ignore_autogenerated + +// Generated for the devfile generator + +// Code generated by helpgen. DO NOT EDIT. + +package getters + +import ( + "sigs.k8s.io/controller-tools/pkg/markers" +) + +func (Generator) Help() *markers.DefinitionHelp { + return &markers.DefinitionHelp{ + Category: "", + DetailedHelp: markers.DetailedHelp{ + Summary: "generates getter methods that are used to return values for the boolean pointer fields. ", + Details: "The pointer receiver is determined from the `devfile:getter:generate` annotated type. The method will return the value of the field if it's been set, otherwise it will return the default value specified by the devfile:default:value annotation.", + }, + FieldHelp: map[string]markers.DetailedHelp{}, + } +} diff --git a/generator/main.go b/generator/main.go index d4c2a77d3..27d72a289 100644 --- a/generator/main.go +++ b/generator/main.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "github.com/devfile/api/generator/getters" "io" "os" "strings" @@ -34,6 +35,7 @@ var ( "deepcopy": deepcopy.Generator{}, "schemas": schemas.Generator{}, "validate": validate.Generator{}, + "getters": getters.Generator{}, } // allOutputRules defines the list of all known output rules, giving @@ -122,6 +124,9 @@ generator overrides:isForPluginOverrides=false paths=./pkg/apis/workspaces/v1alp # Generate Interface Implementations based on the workspaces/v1alpha2 K8S API generator interfaces paths=./pkg/apis/workspaces/v1alpha2 +# Generate Boolean Getter implementations based on the workspaces/v1alpha2 K8S API +generator getters paths=./pkg/apis/workspaces/v1alpha2 + # Generate K8S CRDs based on the workspaces/v1alpha2 K8S API generator crds output:crds:artifacts:config=crds paths=./pkg/apis/workspaces/v1alpha2 diff --git a/generator/overrides/gen.go b/generator/overrides/gen.go index 422a55f7f..283ee72ee 100644 --- a/generator/overrides/gen.go +++ b/generator/overrides/gen.go @@ -227,6 +227,12 @@ func (g Generator) createOverride(newTypeToProcess typeToProcess, packageTypes m ) } + overrideGenDecl.Doc = updateComments( + overrideGenDecl, overrideGenDecl.Doc, + `.*`, + ` *`+regexp.QuoteMeta("+devfile:getter:generate")+`.*`, + ) + overrideGenDecl.Doc = updateComments( overrideGenDecl, overrideGenDecl.Doc, `.*`, @@ -318,6 +324,13 @@ func (g Generator) createOverride(newTypeToProcess typeToProcess, packageTypes m ` *`+regexp.QuoteMeta("+kubebuilder:default")+` *=.*`, ) + //remove the +devfile:default:values for overrides + astField.Doc = updateComments( + astField, astField.Doc, + `.*`, + ` *`+regexp.QuoteMeta("+devfile:default:value")+` *=.*`, + ) + processFieldType := func(ident *ast.Ident) *typeToProcess { typeToOverride, existsInPackage := packageTypes[ident.Name] if !existsInPackage { diff --git a/pkg/apis/workspaces/v1alpha2/commands.go b/pkg/apis/workspaces/v1alpha2/commands.go index 6698a1d30..e99a2dfa9 100644 --- a/pkg/apis/workspaces/v1alpha2/commands.go +++ b/pkg/apis/workspaces/v1alpha2/commands.go @@ -29,12 +29,14 @@ const ( DeployCommandGroupKind CommandGroupKind = "deploy" ) +// +devfile:getter:generate type CommandGroup struct { // Kind of group the command is part of Kind CommandGroupKind `json:"kind"` // +optional // Identifies the default command for a given group kind + // +devfile:default:value=false IsDefault *bool `json:"isDefault,omitempty"` } @@ -107,6 +109,7 @@ type CommandUnion struct { Custom *CustomCommand `json:"custom,omitempty"` } +// +devfile:getter:generate type ExecCommand struct { LabeledCommand `json:",inline"` @@ -145,6 +148,7 @@ type ExecCommand struct { // If set to `true` the command won't be restarted and it is expected to handle file changes on its own. // // Default value is `false` + // +devfile:default:value=false HotReloadCapable *bool `json:"hotReloadCapable,omitempty"` } @@ -156,6 +160,7 @@ type ApplyCommand struct { Component string `json:"component"` } +// +devfile:getter:generate type CompositeCommand struct { LabeledCommand `json:",inline"` @@ -164,6 +169,7 @@ type CompositeCommand struct { // Indicates if the sub-commands should be executed concurrently // +optional + // +devfile:default:value=false Parallel *bool `json:"parallel,omitempty"` } diff --git a/pkg/apis/workspaces/v1alpha2/component_container.go b/pkg/apis/workspaces/v1alpha2/component_container.go index f6168c356..08ded6bb2 100644 --- a/pkg/apis/workspaces/v1alpha2/component_container.go +++ b/pkg/apis/workspaces/v1alpha2/component_container.go @@ -7,6 +7,7 @@ type ContainerComponent struct { Endpoints []Endpoint `json:"endpoints,omitempty" patchStrategy:"merge" patchMergeKey:"name"` } +// +devfile:getter:generate type Container struct { Image string `json:"image"` @@ -69,9 +70,22 @@ type Container struct { // // Default value is `false` // +optional + // +devfile:default:value=false DedicatedPod *bool `json:"dedicatedPod,omitempty"` } +//GetMountSources returns the value of the boolean property. If it's unset, the default value is true for all component types except plugins and components that set `dedicatedPod` to true. +func (in *Container) GetMountSources() bool { + if in.MountSources != nil { + return *in.MountSources + } else { + if in.GetDedicatedPod() { + return false + } + return true + } +} + type EnvVar struct { Name string `json:"name" yaml:"name"` Value string `json:"value" yaml:"value"` diff --git a/pkg/apis/workspaces/v1alpha2/component_image_dockerfile.go b/pkg/apis/workspaces/v1alpha2/component_image_dockerfile.go index 4743825e2..a200ba137 100644 --- a/pkg/apis/workspaces/v1alpha2/component_image_dockerfile.go +++ b/pkg/apis/workspaces/v1alpha2/component_image_dockerfile.go @@ -41,6 +41,7 @@ type DockerfileSrc struct { Git *DockerfileGitProjectSource `json:"git,omitempty"` } +// +devfile:getter:generate type Dockerfile struct { // Path of source directory to establish build context. Defaults to ${PROJECT_ROOT} in the container // +optional @@ -54,6 +55,7 @@ type Dockerfile struct { // // Default value is `false` // +optional + // +devfile:default:value=false RootRequired *bool `json:"rootRequired,omitempty"` } diff --git a/pkg/apis/workspaces/v1alpha2/component_volume.go b/pkg/apis/workspaces/v1alpha2/component_volume.go index b2517499d..c3bd69280 100644 --- a/pkg/apis/workspaces/v1alpha2/component_volume.go +++ b/pkg/apis/workspaces/v1alpha2/component_volume.go @@ -7,6 +7,7 @@ type VolumeComponent struct { } // Volume that should be mounted to a component container +// +devfile:getter:generate type Volume struct { // +optional // Size of the volume @@ -15,5 +16,6 @@ type Volume struct { // +optional // Ephemeral volumes are not stored persistently across restarts. Defaults // to false + // +devfile:default:value=false Ephemeral *bool `json:"ephemeral,omitempty"` } diff --git a/pkg/apis/workspaces/v1alpha2/endpoint.go b/pkg/apis/workspaces/v1alpha2/endpoint.go index 8599a6e4f..3cf10d8ea 100644 --- a/pkg/apis/workspaces/v1alpha2/endpoint.go +++ b/pkg/apis/workspaces/v1alpha2/endpoint.go @@ -1,8 +1,6 @@ package v1alpha2 -import ( - attributes "github.com/devfile/api/v2/pkg/attributes" -) +import "github.com/devfile/api/v2/pkg/attributes" // EndpointProtocol defines the application and transport protocols of the traffic that will go through this endpoint. // Only one of the following protocols may be specified: http, ws, tcp, udp. @@ -46,6 +44,7 @@ const ( NoneEndpointExposure EndpointExposure = "none" ) +// +devfile:getter:generate type Endpoint struct { // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ // +kubebuilder:validation:MaxLength=63 @@ -94,6 +93,7 @@ type Endpoint struct { // Describes whether the endpoint should be secured and protected by some // authentication process. This requires a protocol of `https` or `wss`. // +optional + // +devfile:default:value=false Secure *bool `json:"secure,omitempty"` // Path of the endpoint URL diff --git a/pkg/apis/workspaces/v1alpha2/zz_generated.getters.go b/pkg/apis/workspaces/v1alpha2/zz_generated.getters.go new file mode 100644 index 000000000..4c7c36a30 --- /dev/null +++ b/pkg/apis/workspaces/v1alpha2/zz_generated.getters.go @@ -0,0 +1,43 @@ +package v1alpha2 + +// GetIsDefault returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *CommandGroup) GetIsDefault() bool { + return getBoolOrDefault(in.IsDefault, false) +} + +// GetHotReloadCapable returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *ExecCommand) GetHotReloadCapable() bool { + return getBoolOrDefault(in.HotReloadCapable, false) +} + +// GetParallel returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *CompositeCommand) GetParallel() bool { + return getBoolOrDefault(in.Parallel, false) +} + +// GetDedicatedPod returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *Container) GetDedicatedPod() bool { + return getBoolOrDefault(in.DedicatedPod, false) +} + +// GetRootRequired returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *Dockerfile) GetRootRequired() bool { + return getBoolOrDefault(in.RootRequired, false) +} + +// GetEphemeral returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *Volume) GetEphemeral() bool { + return getBoolOrDefault(in.Ephemeral, false) +} + +// GetSecure returns the value of the boolean property. If unset, it's the default value specified in the devfile:default:value marker +func (in *Endpoint) GetSecure() bool { + return getBoolOrDefault(in.Secure, false) +} + +func getBoolOrDefault(input *bool, defaultVal bool) bool { + if input != nil { + return *input + } + return defaultVal +}