Skip to content

Commit c7c7388

Browse files
committed
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.
1 parent 48527f7 commit c7c7388

File tree

4 files changed

+949
-181
lines changed

4 files changed

+949
-181
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package projectparameter
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/google/uuid"
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/coder/database"
13+
"github.com/coder/coder/provisionersdk/proto"
14+
)
15+
16+
// Scope targets identifiers to pull parameters from.
17+
type Scope struct {
18+
OrganizationID string
19+
ProjectID uuid.UUID
20+
ProjectHistoryID uuid.UUID
21+
UserID string
22+
WorkspaceID uuid.UUID
23+
WorkspaceHistoryID uuid.UUID
24+
}
25+
26+
// Value represents a computed parameter.
27+
type Value struct {
28+
Proto *proto.ParameterValue
29+
// DefaultValue is whether a default value for the scope
30+
// was consumed. This can only be true for projects.
31+
DefaultValue bool
32+
Scope database.ParameterScope
33+
ScopeID string
34+
}
35+
36+
// Compute accepts a scope in which parameter values are sourced.
37+
// These sources are iterated in a hierarchial fashion to determine
38+
// the runtime parameter vaues for a project.
39+
func Compute(ctx context.Context, db database.Store, scope Scope) ([]Value, error) {
40+
compute := &compute{
41+
parameterByName: map[string]Value{},
42+
projectParameterByName: map[string]database.ProjectParameter{},
43+
}
44+
45+
// All parameters for the project version!
46+
projectHistoryParameters, err := db.GetProjectParametersByHistoryID(ctx, scope.ProjectHistoryID)
47+
if errors.Is(err, sql.ErrNoRows) {
48+
// It's valid to have no parameters!
49+
return []Value{}, nil
50+
}
51+
if err != nil {
52+
return nil, xerrors.Errorf("get project parameters: %w", err)
53+
}
54+
for _, projectParameter := range projectHistoryParameters {
55+
compute.projectParameterByName[projectParameter.Name] = projectParameter
56+
}
57+
58+
// Organization parameters come first!
59+
organizationParameters, err := db.GetParameterValuesByScope(ctx, database.GetParameterValuesByScopeParams{
60+
Scope: database.ParameterScopeOrganization,
61+
ScopeID: scope.OrganizationID,
62+
})
63+
if errors.Is(err, sql.ErrNoRows) {
64+
err = nil
65+
}
66+
if err != nil {
67+
return nil, xerrors.Errorf("get organization parameters: %w", err)
68+
}
69+
err = compute.inject(organizationParameters)
70+
if err != nil {
71+
return nil, xerrors.Errorf("inject organization parameters: %w", err)
72+
}
73+
74+
// Default project parameter values come second!
75+
for _, projectParameter := range projectHistoryParameters {
76+
if !projectParameter.DefaultSourceValue.Valid {
77+
continue
78+
}
79+
if !projectParameter.DefaultDestinationValue.Valid {
80+
continue
81+
}
82+
83+
destinationScheme, err := convertDestinationScheme(projectParameter.DefaultDestinationScheme)
84+
if err != nil {
85+
return nil, xerrors.Errorf("convert default destination scheme for project parameter %q: %w", projectParameter.Name, err)
86+
}
87+
88+
switch projectParameter.DefaultSourceScheme {
89+
case database.ParameterSourceSchemeData:
90+
compute.parameterByName[projectParameter.Name] = Value{
91+
Proto: &proto.ParameterValue{
92+
DestinationScheme: destinationScheme,
93+
Name: projectParameter.DefaultDestinationValue.String,
94+
Value: projectParameter.DefaultSourceValue.String,
95+
},
96+
DefaultValue: true,
97+
Scope: database.ParameterScopeProject,
98+
ScopeID: scope.ProjectID.String(),
99+
}
100+
default:
101+
return nil, xerrors.Errorf("unsupported source scheme for project parameter %q: %q", projectParameter.Name, string(projectParameter.DefaultSourceScheme))
102+
}
103+
}
104+
105+
// Project parameters come third!
106+
projectParameters, err := db.GetParameterValuesByScope(ctx, database.GetParameterValuesByScopeParams{
107+
Scope: database.ParameterScopeProject,
108+
ScopeID: scope.ProjectID.String(),
109+
})
110+
if errors.Is(err, sql.ErrNoRows) {
111+
err = nil
112+
}
113+
if err != nil {
114+
return nil, xerrors.Errorf("get project parameters: %w", err)
115+
}
116+
err = compute.inject(projectParameters)
117+
if err != nil {
118+
return nil, xerrors.Errorf("inject project parameters: %w", err)
119+
}
120+
121+
// User parameters come fourth!
122+
userParameters, err := db.GetParameterValuesByScope(ctx, database.GetParameterValuesByScopeParams{
123+
Scope: database.ParameterScopeUser,
124+
ScopeID: scope.UserID,
125+
})
126+
if errors.Is(err, sql.ErrNoRows) {
127+
err = nil
128+
}
129+
if err != nil {
130+
return nil, xerrors.Errorf("get user parameters: %w", err)
131+
}
132+
err = compute.inject(userParameters)
133+
if err != nil {
134+
return nil, xerrors.Errorf("inject user parameters: %w", err)
135+
}
136+
137+
// Workspace parameters come last!
138+
workspaceParameters, err := db.GetParameterValuesByScope(ctx, database.GetParameterValuesByScopeParams{
139+
Scope: database.ParameterScopeWorkspace,
140+
ScopeID: scope.WorkspaceID.String(),
141+
})
142+
if errors.Is(err, sql.ErrNoRows) {
143+
err = nil
144+
}
145+
if err != nil {
146+
return nil, xerrors.Errorf("get workspace parameters: %w", err)
147+
}
148+
err = compute.inject(workspaceParameters)
149+
if err != nil {
150+
return nil, xerrors.Errorf("inject workspace parameters: %w", err)
151+
}
152+
153+
for _, projectParameter := range compute.projectParameterByName {
154+
if _, ok := compute.parameterByName[projectParameter.Name]; ok {
155+
continue
156+
}
157+
return nil, NoValueError{
158+
ParameterID: projectParameter.ID,
159+
ParameterName: projectParameter.Name,
160+
}
161+
}
162+
163+
values := make([]Value, 0, len(compute.parameterByName))
164+
for _, value := range compute.parameterByName {
165+
values = append(values, value)
166+
}
167+
return values, nil
168+
}
169+
170+
type compute struct {
171+
parameterByName map[string]Value
172+
projectParameterByName map[string]database.ProjectParameter
173+
}
174+
175+
// Validates and computes the value for parameters; setting the value on "parameterByName".
176+
func (c *compute) inject(scopedParameters []database.ParameterValue) error {
177+
for _, scopedParameter := range scopedParameters {
178+
projectParameter, hasProjectParameter := c.projectParameterByName[scopedParameter.Name]
179+
if !hasProjectParameter {
180+
// Don't inject parameters that aren't defined by the project.
181+
continue
182+
}
183+
184+
_, hasExistingParameter := c.parameterByName[scopedParameter.Name]
185+
if hasExistingParameter {
186+
// If a parameter already exists, check if this variable can override it.
187+
// Injection hierarchy is the responsibility of the caller. This check ensures
188+
// project parameters cannot be overridden if already set.
189+
if !projectParameter.AllowOverrideSource && scopedParameter.Scope != database.ParameterScopeProject {
190+
continue
191+
}
192+
}
193+
194+
destinationScheme, err := convertDestinationScheme(scopedParameter.DestinationScheme)
195+
if err != nil {
196+
return xerrors.Errorf("convert destination scheme: %w", err)
197+
}
198+
199+
switch scopedParameter.SourceScheme {
200+
case database.ParameterSourceSchemeData:
201+
c.parameterByName[projectParameter.Name] = Value{
202+
Proto: &proto.ParameterValue{
203+
DestinationScheme: destinationScheme,
204+
Name: scopedParameter.SourceValue,
205+
Value: scopedParameter.DestinationValue,
206+
},
207+
}
208+
default:
209+
return xerrors.Errorf("unsupported source scheme: %q", string(projectParameter.DefaultSourceScheme))
210+
}
211+
}
212+
return nil
213+
}
214+
215+
// Converts the database destination scheme to the protobuf version.
216+
func convertDestinationScheme(scheme database.ParameterDestinationScheme) (proto.ParameterDestination_Scheme, error) {
217+
switch scheme {
218+
case database.ParameterDestinationSchemeEnvironmentVariable:
219+
return proto.ParameterDestination_ENVIRONMENT_VARIABLE, nil
220+
case database.ParameterDestinationSchemeProvisionerVariable:
221+
return proto.ParameterDestination_PROVISIONER_VARIABLE, nil
222+
default:
223+
return 0, xerrors.Errorf("unsupported destination scheme: %q", scheme)
224+
}
225+
}
226+
227+
type NoValueError struct {
228+
ParameterID uuid.UUID
229+
ParameterName string
230+
}
231+
232+
func (e NoValueError) Error() string {
233+
return fmt.Sprintf("no value for parameter %q found", e.ParameterName)
234+
}

0 commit comments

Comments
 (0)