Files
coder/coderd/projectparameter/projectparameter.go
T
Kyle Carberry 7364933e65 refactor: Allow provisioner jobs to be disconnected from projects (#194)
* Nest jobs under an organization

* Rename project parameter to parameter schema

* Update references when computing project parameters

* Add files endpoint

* Allow one-off project import jobs

* Allow variables to be injected that are not defined by the schema

* Update API to use jobs first

* Fix CLI tests

* Fix linting

* Fix hex length for files table

* Reduce memory allocation for windows
2022-02-08 12:00:44 -06:00

234 lines
7.3 KiB
Go

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 {
ImportJobID uuid.UUID
OrganizationID string
ProjectID uuid.NullUUID
UserID sql.NullString
WorkspaceID uuid.NullUUID
}
// 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, additional ...database.ParameterValue) ([]Value, error) {
compute := &compute{
db: db,
computedParameterByName: map[string]Value{},
parameterSchemasByName: map[string]database.ParameterSchema{},
}
// All parameters for the import job ID!
parameterSchemas, err := db.GetParameterSchemasByJobID(ctx, scope.ImportJobID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
return nil, xerrors.Errorf("get project parameters: %w", err)
}
for _, projectVersionParameter := range parameterSchemas {
compute.parameterSchemasByName[projectVersionParameter.Name] = projectVersionParameter
}
// Organization parameters come first!
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeOrganization,
ScopeID: scope.OrganizationID,
})
if err != nil {
return nil, err
}
// Default project parameter values come second!
for _, projectVersionParameter := range parameterSchemas {
if !projectVersionParameter.DefaultSourceValue.Valid {
continue
}
if !projectVersionParameter.DefaultDestinationValue.Valid {
continue
}
destinationScheme, err := convertDestinationScheme(projectVersionParameter.DefaultDestinationScheme)
if err != nil {
return nil, xerrors.Errorf("convert default destination scheme for project version parameter %q: %w", projectVersionParameter.Name, err)
}
switch projectVersionParameter.DefaultSourceScheme {
case database.ParameterSourceSchemeData:
compute.computedParameterByName[projectVersionParameter.Name] = Value{
Proto: &proto.ParameterValue{
DestinationScheme: destinationScheme,
Name: projectVersionParameter.DefaultDestinationValue.String,
Value: projectVersionParameter.DefaultSourceValue.String,
},
DefaultValue: true,
Scope: database.ParameterScopeProject,
ScopeID: scope.ProjectID.UUID.String(),
}
default:
return nil, xerrors.Errorf("unsupported source scheme for project version parameter %q: %q", projectVersionParameter.Name, string(projectVersionParameter.DefaultSourceScheme))
}
}
if scope.ProjectID.Valid {
// Project parameters come third!
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeProject,
ScopeID: scope.ProjectID.UUID.String(),
})
if err != nil {
return nil, err
}
}
if scope.UserID.Valid {
// User parameters come fourth!
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeUser,
ScopeID: scope.UserID.String,
})
if err != nil {
return nil, err
}
}
if scope.WorkspaceID.Valid {
// Workspace parameters come last!
err = compute.injectScope(ctx, database.GetParameterValuesByScopeParams{
Scope: database.ParameterScopeWorkspace,
ScopeID: scope.WorkspaceID.UUID.String(),
})
if err != nil {
return nil, err
}
}
for _, parameterValue := range additional {
err = compute.injectSingle(parameterValue)
if err != nil {
return nil, xerrors.Errorf("inject %q: %w", parameterValue.Name, err)
}
}
for _, projectVersionParameter := range compute.parameterSchemasByName {
if _, ok := compute.computedParameterByName[projectVersionParameter.Name]; ok {
continue
}
return nil, NoValueError{
ParameterID: projectVersionParameter.ID,
ParameterName: projectVersionParameter.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
parameterSchemasByName map[string]database.ParameterSchema
}
// Validates and computes the value for parameters; setting the value on "parameterByName".
func (c *compute) injectScope(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 {
err = c.injectSingle(scopedParameter)
if err != nil {
return xerrors.Errorf("inject single %q: %w", scopedParameter.Name, err)
}
}
return nil
}
func (c *compute) injectSingle(scopedParameter database.ParameterValue) error {
parameterSchema, hasParameterSchema := c.parameterSchemasByName[scopedParameter.Name]
if hasParameterSchema {
// Don't inject parameters that aren't defined by the project.
_, 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 !parameterSchema.AllowOverrideSource && scopedParameter.Scope != database.ParameterScopeProject {
return nil
}
}
}
destinationScheme, err := convertDestinationScheme(scopedParameter.DestinationScheme)
if err != nil {
return xerrors.Errorf("convert destination scheme: %w", err)
}
switch scopedParameter.SourceScheme {
case database.ParameterSourceSchemeData:
c.computedParameterByName[scopedParameter.Name] = Value{
Proto: &proto.ParameterValue{
DestinationScheme: destinationScheme,
Name: scopedParameter.SourceValue,
Value: scopedParameter.DestinationValue,
},
}
default:
return xerrors.Errorf("unsupported source scheme: %q", string(parameterSchema.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)
}