Skip to content

Commit

Permalink
feat: Compute project build parameters (#82)
Browse files Browse the repository at this point in the history
* feat: Add parameter and jobs database schema

This modifies a prior migration which is typically forbidden,
but because we're pre-production deployment I felt grouping
would be helpful to future contributors.

This adds database functions that are required for the provisioner
daemon and job queue logic.

* feat: Compute project build parameters

Adds a projectparameter package to compute build-time project
values for a provided scope.

This package will be used to return which variables are being
used for a build, and can visually indicate the hierarchy to
a user.

* Fix terraform provisioner

* Improve naming, abstract inject to consume scope

* Run CI on all branches
  • Loading branch information
kylecarbs committed Jan 29, 2022
1 parent b503c8b commit b3c5bb3
Show file tree
Hide file tree
Showing 9 changed files with 969 additions and 191 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/coder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ on:

pull_request:
branches:
- main
- "release/*"
- "*"

workflow_dispatch:

Expand Down
217 changes: 217 additions & 0 deletions coderd/projectparameter/projectparameter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package projectparameter

import (
"context"
"database/sql"
"errors"
"fmt"

"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/database"
"github.com/coder/coder/provisionersdk/proto"
)

// Scope targets identifiers to pull parameters from.
type Scope struct {
OrganizationID string
ProjectID uuid.UUID
ProjectHistoryID uuid.UUID
UserID string
WorkspaceID uuid.UUID
WorkspaceHistoryID uuid.UUID
}

// Value represents a computed parameter.
type Value struct {
Proto *proto.ParameterValue
// DefaultValue is whether a default value for the scope
// was consumed. This can only be true for projects.
DefaultValue bool
Scope database.ParameterScope
ScopeID string
}

// Compute accepts a scope in which parameter values are sourced.
// These sources are iterated in a hierarchical fashion to determine
// the runtime parameter values for a project.
func Compute(ctx context.Context, db database.Store, scope Scope) ([]Value, error) {
compute := &compute{
db: db,
computedParameterByName: map[string]Value{},
projectHistoryParametersByName: map[string]database.ProjectParameter{},
}

// All parameters for the project version!
projectHistoryParameters, err := db.GetProjectParametersByHistoryID(ctx, scope.ProjectHistoryID)
if errors.Is(err, sql.ErrNoRows) {
// This occurs when the project history has defined
// no parameters, so we have nothing to compute!
return []Value{}, nil
}
if err != nil {
return nil, xerrors.Errorf("get project parameters: %w", err)
}
for _, projectHistoryParameter := range projectHistoryParameters {
compute.projectHistoryParametersByName[projectHistoryParameter.Name] = projectHistoryParameter
}

// Organization parameters come first!
err = compute.inject(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeOrganization,
ScopeID: scope.OrganizationID,
})
if err != nil {
return nil, err
}

// Default project parameter values come second!
for _, projectHistoryParameter := range projectHistoryParameters {
if !projectHistoryParameter.DefaultSourceValue.Valid {
continue
}
if !projectHistoryParameter.DefaultDestinationValue.Valid {
continue
}

destinationScheme, err := convertDestinationScheme(projectHistoryParameter.DefaultDestinationScheme)
if err != nil {
return nil, xerrors.Errorf("convert default destination scheme for project history parameter %q: %w", projectHistoryParameter.Name, err)
}

switch projectHistoryParameter.DefaultSourceScheme {
case database.ParameterSourceSchemeData:
compute.computedParameterByName[projectHistoryParameter.Name] = Value{
Proto: &proto.ParameterValue{
DestinationScheme: destinationScheme,
Name: projectHistoryParameter.DefaultDestinationValue.String,
Value: projectHistoryParameter.DefaultSourceValue.String,
},
DefaultValue: true,
Scope: database.ParameterScopeProject,
ScopeID: scope.ProjectID.String(),
}
default:
return nil, xerrors.Errorf("unsupported source scheme for project history parameter %q: %q", projectHistoryParameter.Name, string(projectHistoryParameter.DefaultSourceScheme))
}
}

// Project parameters come third!
err = compute.inject(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeProject,
ScopeID: scope.ProjectID.String(),
})
if err != nil {
return nil, err
}

// User parameters come fourth!
err = compute.inject(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeUser,
ScopeID: scope.UserID,
})
if err != nil {
return nil, err
}

// Workspace parameters come last!
err = compute.inject(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeWorkspace,
ScopeID: scope.WorkspaceID.String(),
})
if err != nil {
return nil, err
}

for _, projectHistoryParameter := range compute.projectHistoryParametersByName {
if _, ok := compute.computedParameterByName[projectHistoryParameter.Name]; ok {
continue
}
return nil, NoValueError{
ParameterID: projectHistoryParameter.ID,
ParameterName: projectHistoryParameter.Name,
}
}

values := make([]Value, 0, len(compute.computedParameterByName))
for _, value := range compute.computedParameterByName {
values = append(values, value)
}
return values, nil
}

type compute struct {
db database.Store
computedParameterByName map[string]Value
projectHistoryParametersByName map[string]database.ProjectParameter
}

// Validates and computes the value for parameters; setting the value on "parameterByName".
func (c *compute) inject(ctx context.Context, scopeParams database.GetParameterValuesByScopeParams) error {
scopedParameters, err := c.db.GetParameterValuesByScope(ctx, scopeParams)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
return xerrors.Errorf("get %s parameters: %w", scopeParams.Scope, err)
}

for _, scopedParameter := range scopedParameters {
projectHistoryParameter, hasProjectHistoryParameter := c.projectHistoryParametersByName[scopedParameter.Name]
if !hasProjectHistoryParameter {
// Don't inject parameters that aren't defined by the project.
continue
}

_, hasExistingParameter := c.computedParameterByName[scopedParameter.Name]
if hasExistingParameter {
// If a parameter already exists, check if this variable can override it.
// Injection hierarchy is the responsibility of the caller. This check ensures
// project parameters cannot be overridden if already set.
if !projectHistoryParameter.AllowOverrideSource && scopedParameter.Scope != database.ParameterScopeProject {
continue
}
}

destinationScheme, err := convertDestinationScheme(scopedParameter.DestinationScheme)
if err != nil {
return xerrors.Errorf("convert destination scheme: %w", err)
}

switch scopedParameter.SourceScheme {
case database.ParameterSourceSchemeData:
c.computedParameterByName[projectHistoryParameter.Name] = Value{
Proto: &proto.ParameterValue{
DestinationScheme: destinationScheme,
Name: scopedParameter.SourceValue,
Value: scopedParameter.DestinationValue,
},
}
default:
return xerrors.Errorf("unsupported source scheme: %q", string(projectHistoryParameter.DefaultSourceScheme))
}
}
return nil
}

// Converts the database destination scheme to the protobuf version.
func convertDestinationScheme(scheme database.ParameterDestinationScheme) (proto.ParameterDestination_Scheme, error) {
switch scheme {
case database.ParameterDestinationSchemeEnvironmentVariable:
return proto.ParameterDestination_ENVIRONMENT_VARIABLE, nil
case database.ParameterDestinationSchemeProvisionerVariable:
return proto.ParameterDestination_PROVISIONER_VARIABLE, nil
default:
return 0, xerrors.Errorf("unsupported destination scheme: %q", scheme)
}
}

type NoValueError struct {
ParameterID uuid.UUID
ParameterName string
}

func (e NoValueError) Error() string {
return fmt.Sprintf("no value for parameter %q found", e.ParameterName)
}

0 comments on commit b3c5bb3

Please sign in to comment.