Skip to content

Commit

Permalink
Fixing Deployment Secrets - Plaintext in Inputs (#325)
Browse files Browse the repository at this point in the history
### Note: there is an alternative implementation of this PR -
#320

### Summary

- Added logic to save ciphertext into the output properties for secret
values, allowing comparison on refresh of just ciphertext, fixing
#123
- Import now works as well, including code generation (with dummy values
for secrets)
- Migrated to new PUT API and updated client to actually return
DeploymentSettings

### Testing
- Tested pulumi up, refresh, import and up from previous version of the
provider (for unchanged DS inputs, migrating to this new way of saving
will require refresh and then up)

Example TS program (Sadly can't use Dotnet, due to bug with maps):
```
const settings = new service.DeploymentSettings("deployment_settings", {
  organization: "IaroslavTitov",
  project: "PulumiDotnet",
  stack: "SdkTest5",
  operationContext: {
    environmentVariables: {
      TEST_VAR: "fooa",
      SECRET_VAR: config.requireSecret("my_secret"),
    }
  },
  sourceContext: {
    git: {
        repoUrl: "https://github.com/pulumi/deploy-demos.git",
        branch: "refs/heads/main",
        repoDir: "pulumi-programs/simple-resource",
        gitAuth: {
            sshAuth: {
                sshPrivateKey: "privatekey",
                password: secret,
            }
        }
    }
}
});
```

Secret resource values end up with just cipher, while plaintext is
stored in inputs:
```
      "sshAuth": {
        "password": "AAABAD6/2Nroj62qORoHOLofFOkRhdUNwCAYeC86nABU/G4AO5I7Fw==",
        "sshPrivateKey": "AAABABqRIQ1bZbvU/hrlpX1Rh9sj9OCyArjG0SUILPQmb0KSCFIrz6bK"
      }
```

Passwords and sshKey are forced into twin secrets, Environment Variables
are optionally twin secrets, everything else uses normal Pulumi
workflows, because they are not secret in Pulumi Service.

Import of the above code generates successfully with dummy values for
secrets:
```
const ds1 = new pulumiservice.DeploymentSettings("ds1", {
    operationContext: {
        environmentVariables: {
            SECRET_VAR: pulumi.secret("<REPLACE WITH ACTUAL SECRET VALUE>"),
            TEST_VAR: "fooa",
        },
    },
    organization: "IaroslavTitov",
    project: "PulumiDotnet",
    sourceContext: {
        git: {
            branch: "refs/heads/main",
            gitAuth: {
                sshAuth: {
                    password: pulumi.secret("<REPLACE WITH ACTUAL SECRET VALUE>"),
                    sshPrivateKey: pulumi.secret("<REPLACE WITH ACTUAL SECRET VALUE>"),
                },
            },
            repoDir: "pulumi-programs/simple-resource",
            repoUrl: "https://github.com/pulumi/deploy-demos.git",
        },
    },
    stack: "SdkTest5",
}, {
    protect: true,
});
```
  • Loading branch information
IaroslavTitov committed Jun 26, 2024
1 parent 5f7e42d commit 6853f00
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Improvements

### Bug Fixes
- Introduced secrets in Deployment Settings, fixed refresh and import [#123](https://github.com/pulumi/pulumi-pulumiservice/issues/123)

### Miscellaneous
10 changes: 6 additions & 4 deletions provider/pkg/internal/pulumiapi/deployment_setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestGetDeploymentSettings(t *testing.T) {
dsValue := DeploymentSettings{
OperationContext: &OperationContext{},
GitHub: &GitHubConfiguration{},
SourceContext: &apitype.SourceContext{},
SourceContext: &SourceContext{},
ExecutorContext: &apitype.ExecutorContext{},
}

Expand Down Expand Up @@ -74,24 +74,26 @@ func TestCreateDeploymentSettings(t *testing.T) {
dsValue := DeploymentSettings{
OperationContext: &OperationContext{},
GitHub: &GitHubConfiguration{},
SourceContext: &apitype.SourceContext{},
SourceContext: &SourceContext{},
ExecutorContext: &apitype.ExecutorContext{},
}

c, cleanup := startTestServer(t, testServerConfig{
ExpectedReqMethod: http.MethodPost,
ExpectedReqMethod: http.MethodPut,
ExpectedReqPath: "/" + path.Join("api", "stacks", orgName, projectName, stackName, "deployments", "settings"),
ResponseCode: 201,
ExpectedReqBody: dsValue,
ResponseBody: dsValue,
})
defer cleanup()

err := c.CreateDeploymentSettings(ctx, StackName{
response, err := c.CreateDeploymentSettings(ctx, StackName{
OrgName: orgName,
ProjectName: projectName,
StackName: stackName,
}, dsValue)

assert.Nil(t, err)
assert.Equal(t, dsValue, *response)
})
}
97 changes: 82 additions & 15 deletions provider/pkg/internal/pulumiapi/deployment_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package pulumiapi

import (
"context"
"encoding/json"
"fmt"
"net/http"
"path"
Expand All @@ -10,25 +11,26 @@ import (
)

type DeploymentSettingsClient interface {
CreateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) error
UpdateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) error
CreateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) (*DeploymentSettings, error)
UpdateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) (*DeploymentSettings, error)
GetDeploymentSettings(ctx context.Context, stack StackName) (*DeploymentSettings, error)
DeleteDeploymentSettings(ctx context.Context, stack StackName) error
}

type DeploymentSettings struct {
OperationContext *OperationContext `json:"operationContext,omitempty"`
GitHub *GitHubConfiguration `json:"gitHub,omitempty"`
SourceContext *apitype.SourceContext `json:"sourceContext,omitempty"`
SourceContext *SourceContext `json:"sourceContext,omitempty"`
ExecutorContext *apitype.ExecutorContext `json:"executorContext,omitempty"`
AgentPoolId string `json:"agentPoolId,omitempty"`
Source *string `json:"source,omitempty"`
}

type OperationContext struct {
Options *OperationContextOptions `json:"options,omitempty"`
PreRunCommands []string `json:"PreRunCommands,omitempty"`
EnvironmentVariables map[string]apitype.SecretValue `json:"environmentVariables,omitempty"`
OIDC *OIDCConfiguration `json:"oidc,omitempty"`
Options *OperationContextOptions `json:"options,omitempty"`
PreRunCommands []string `json:"PreRunCommands,omitempty"`
EnvironmentVariables map[string]SecretValue `json:"environmentVariables,omitempty"`
OIDC *OIDCConfiguration `json:"oidc,omitempty"`
}

type OIDCConfiguration struct {
Expand Down Expand Up @@ -74,22 +76,87 @@ type GitHubConfiguration struct {
Paths []string `json:"paths,omitempty"`
}

func (c *Client) CreateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) error {
type SourceContext struct {
Git *SourceContextGit `json:"git,omitempty"`
}

type SourceContextGit struct {
RepoURL string `json:"repoURL"`
Branch string `json:"branch"`
RepoDir string `json:"repoDir,omitempty"`
Commit string `json:"commit,omitempty"`
GitAuth *GitAuthConfig `json:"gitAuth,omitempty"`
}

type GitAuthConfig struct {
PersonalAccessToken *SecretValue `json:"accessToken,omitempty"`
SSHAuth *SSHAuth `json:"sshAuth,omitempty"`
BasicAuth *BasicAuth `json:"basicAuth,omitempty"`
}

type SSHAuth struct {
SSHPrivateKey SecretValue `json:"sshPrivateKey"`
Password *SecretValue `json:"password,omitempty"`
}

type BasicAuth struct {
UserName SecretValue `json:"userName"`
Password SecretValue `json:"password"`
}

type SecretValue struct {
Value string // Plaintext if Secret is false; ciphertext otherwise.
Secret bool
}

type secretCiphertextValue struct {
Ciphertext string `json:"ciphertext"`
}

type secretWorkflowValue struct {
Secret string `json:"secret" yaml:"secret"`
}

func (v SecretValue) MarshalJSON() ([]byte, error) {
if v.Secret {
return json.Marshal(secretWorkflowValue{Secret: v.Value})
}
return json.Marshal(v.Value)
}

func (v *SecretValue) UnmarshalJSON(bytes []byte) error {
var secret secretCiphertextValue
if err := json.Unmarshal(bytes, &secret); err == nil {
v.Value, v.Secret = secret.Ciphertext, true
return nil
}

var plaintext string
if err := json.Unmarshal(bytes, &plaintext); err != nil {
return err
}
v.Value, v.Secret = plaintext, false
return nil
}

func (c *Client) CreateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) (*DeploymentSettings, error) {
apiPath := path.Join("stacks", stack.OrgName, stack.ProjectName, stack.StackName, "deployments", "settings")
_, err := c.do(ctx, http.MethodPost, apiPath, ds, nil)
var resultDS = &DeploymentSettings{}
_, err := c.do(ctx, http.MethodPut, apiPath, ds, resultDS)
if err != nil {
return fmt.Errorf("failed to create deployment settings for stack (%s): %w", stack.String(), err)
return nil, fmt.Errorf("failed to create deployment settings for stack (%s): %w", stack.String(), err)
}
return nil
return resultDS, nil
}

func (c *Client) UpdateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) error {
func (c *Client) UpdateDeploymentSettings(ctx context.Context, stack StackName, ds DeploymentSettings) (*DeploymentSettings, error) {
apiPath := path.Join("stacks", stack.OrgName, stack.ProjectName, stack.StackName, "deployments", "settings")
_, err := c.do(ctx, http.MethodPut, apiPath, ds, nil)
var resultDS = &DeploymentSettings{}
_, err := c.do(ctx, http.MethodPut, apiPath, ds, resultDS)
if err != nil {
return fmt.Errorf("failed to update deployment settings for stack (%s): %w", stack.String(), err)
return nil, fmt.Errorf("failed to update deployment settings for stack (%s): %w", stack.String(), err)
}
return nil
return resultDS, nil
}

func (c *Client) GetDeploymentSettings(ctx context.Context, stack StackName) (*DeploymentSettings, error) {
Expand Down
10 changes: 5 additions & 5 deletions provider/pkg/provider/deployment_setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ type DeploymentSettingsClientMock struct {
getDeploymentSettingsFunc getDeploymentSettingsFunc
}

func (c *DeploymentSettingsClientMock) CreateDeploymentSettings(ctx context.Context, stack pulumiapi.StackName, ds pulumiapi.DeploymentSettings) error {
return nil
func (c *DeploymentSettingsClientMock) CreateDeploymentSettings(ctx context.Context, stack pulumiapi.StackName, ds pulumiapi.DeploymentSettings) (*pulumiapi.DeploymentSettings, error) {
return nil, nil
}
func (c *DeploymentSettingsClientMock) UpdateDeploymentSettings(ctx context.Context, stack pulumiapi.StackName, ds pulumiapi.DeploymentSettings) error {
return nil
func (c *DeploymentSettingsClientMock) UpdateDeploymentSettings(ctx context.Context, stack pulumiapi.StackName, ds pulumiapi.DeploymentSettings) (*pulumiapi.DeploymentSettings, error) {
return nil, nil
}
func (c *DeploymentSettingsClientMock) GetDeploymentSettings(ctx context.Context, stack pulumiapi.StackName) (*pulumiapi.DeploymentSettings, error) {
return c.getDeploymentSettingsFunc()
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestDeploymentSettings(t *testing.T) {
return &pulumiapi.DeploymentSettings{
OperationContext: &pulumiapi.OperationContext{},
GitHub: &pulumiapi.GitHubConfiguration{},
SourceContext: &apitype.SourceContext{},
SourceContext: &pulumiapi.SourceContext{},
ExecutorContext: &apitype.ExecutorContext{},
}, nil
},
Expand Down
Loading

0 comments on commit 6853f00

Please sign in to comment.