Skip to content

Commit

Permalink
[pkov2] agent RPC server (#624)
Browse files Browse the repository at this point in the history
<!--Thanks for your contribution. See [CONTRIBUTING](CONTRIBUTING.md)
    for Pulumi's contribution guidelines.

    Help us merge your changes more quickly by adding more details such
    as labels, milestones, and reviewers.-->

### Proposed changes

**Epic Link**:
#606

**Demo Video Link**:
https://pulumi.slack.com/archives/C07DQSV84DC/p1722636430008649

Implements an agent consisting of two commands:

- `init` - fetches a flux source into the given directory, intended for
use in an init container.
- `serve` - starts an RPC server providing an automation API to perform
stack updates over the given workspace.

### Overview
The RPC server assumes that the project source code has been checked out
to a local working directory, called the "workspace" directory. This
generally corresponds to a sub-directory within a git repository, e.g.
[examples/random-yaml](https://github.com/pulumi/examples/tree/master/random-yaml).

At startup, the server opens the workspace using
[auto.NewLocalWorkspace](https://github.com/pulumi/pulumi/blob/5651750bb254f73da5ef0fa503818c5a38755ea8/sdk/go/auto/local_workspace.go#L848).
All RPC operations are applied to this same workspace, usually
one-at-a-time. Some operations cause state changes, e.g. stack
selection, that may affect subsequent operations. Some operations
produce `PreconditionFailed` if a stack hasn't been selected.

At startup, the server optionally runs `pulumi install` to install
dependencies and plugins for the project, based on
pulumi/pulumi#16782. Note that PKOv1 has some
code to install plugins, now believed to be obsolete (see
[discussion](pulumi/pulumi#16782 (comment))).

The supported operations are:

- `WhoAmI` - returns current user info.
- `Install` - runs `pulumi install` in the workspace.
- `SelectStack` - select (and optionally create) a stack, for use in
subsequent operations.
- `Info` - a summary of the current stack.
- `SetAllConfig` - set multiple configuration values on the current
stack, based on literals, environment variables, and file references. It
is expected that the server's pod would have ConfigMaps and Secrets
mounted accordingly.
- `Preview` runs the preview operation for the current stack.
- `Up` runs the up operation for the current stack.
- `Destroy` runs the destroy operation for the current stack.
- `Refresh` runs the refresh operation for the current stack.

The deployment operations have streaming responses, consisting of a
series of engine events and a final result.

The agent uses zap for logging, because it supports structured logging,
implements `io.Writer` to capture Pulumi console output, and integrates
well with grpc-go.


### Follow-ups

- [x] Write RPC server tests
- [ ] Rename 'init' to 'fetch' for clarity
- [ ] lock the workspace during an operation? Or rely on locking within
the Pulumi CLI?

### Related issues (optional)

<!--Refer to related PRs or issues: #1234, or 'Fixes #1234' or 'Closes
#1234'.
Or link to full URLs to issues or pull requests in other GitHub
repositories. -->

Closes #610 #611
  • Loading branch information
EronWright authored Aug 8, 2024
1 parent 13b81b8 commit 5b5d8a7
Show file tree
Hide file tree
Showing 25 changed files with 6,160 additions and 5 deletions.
54 changes: 54 additions & 0 deletions .github/workflows/v2-run-acceptance-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: Pulumi Kubernetes Operator PR Builds
on:
pull_request:
branches:
- v2
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:

build:
runs-on: ubuntu-latest
name: Build
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
# 'latest', 'nightly', or a semver
version: '~> v2'
args: release --snapshot --clean --skip=docker

agent-integration-tests:
runs-on: ubuntu-latest
name: Integration Testing
if: github.event_name == 'repository_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
- name: Install Pulumi
uses: pulumi/actions@v5
- name: Run Tests
run: make -C agent test
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
with:
file: agent/coverage.out
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
45 changes: 44 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
version: 2
project_name: pulumi-kubernetes-operator

builds:
Expand All @@ -18,6 +19,25 @@ builds:
main: ./cmd/manager/main.go
binary: pulumi-kubernetes-operator

- id: pulumi-kubernetes-agent
ldflags:
- -X github.com/pulumi/pulumi-kubernetes-operator/agent/version.Version={{.Version}}
- -w -extldflags "-static"
flags:
- -a
- -tags
- netgo
goos:
- linux
goarch:
- amd64
env:
- CGO_ENABLED=0
- GO111MODULE=on
main: ./main.go
dir: agent
binary: pulumi-kubernetes-agent

archives:
- name_template: >-
{{ .ProjectName }}_
Expand All @@ -44,7 +64,7 @@ release:
name_template: "{{.ProjectName}}-v{{.Version}}"

dockers:
-
- id: pulumi-kubernetes-operator
# GOOS of the built binary that should be used.
goos: linux

Expand All @@ -65,3 +85,26 @@ dockers:
- "--label=org.label-schema.name={{ .ProjectName }}"
- "--label=org.label-schema.vcs-ref={{ .ShortCommit }}"
- "--label=org.label-schema.vcs-url='{{ .GitURL }}'"

- id: pulumi-kubernetes-agent
# GOOS of the built binary that should be used.
goos: linux

# GOARCH of the built binary that should be used.
goarch: amd64

# Path to the Dockerfile (from the project root).
dockerfile: agent/Dockerfile

# Templates of the Docker image names.
image_templates:
- "pulumi/pulumi-kubernetes-agent:latest"
- "pulumi/pulumi-kubernetes-agent:{{ .Version }}"

build_flag_templates:
- "--pull"
- "--label=org.label-schema.build-date={{.Date}}"
- "--label=org.label-schema.name={{ .ProjectName }}"
- "--label=org.label-schema.vcs-ref={{ .ShortCommit }}"
- "--label=org.label-schema.vcs-url='{{ .GitURL }}'"

23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@
"args": [
"--zap-level=debug"
]
},
{
"name": "Agent",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "agent",
"args": [
"serve",
"-v=false",
"--workspace=${input:workdir}",
"-s=dev"
],
"env": {
"AWS_REGION": "us-west-1",
}
}
],
"inputs": [
{
"id": "workdir",
"description": "Please provide the Pulumi program directory",
"type": "promptString"
}
]
}
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,7 @@ version:
dep-tidy:
go mod tidy

.PHONY: build build-static codegen generate-crds install-crds generate-k8s test version dep-tidy build-image push-image push-image-latest deploy prep-spec
agent:
cd agent && $(MAKE) agent

.PHONY: build build-static codegen generate-crds install-crds generate-k8s test version dep-tidy build-image push-image push-image-latest deploy prep-spec agent
2 changes: 2 additions & 0 deletions agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pulumi-kubernetes-agent
coverage.out
34 changes: 34 additions & 0 deletions agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Build the agent binary
FROM golang:1.22 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG VERSION

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum

# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the go source
COPY main.go main.go
COPY cmd/ cmd/
COPY pkg/ pkg/
COPY version/ version/

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags "-X github.com/pulumi/pulumi-kubernetes-operator/agent/version.Version=${VERSION}" -a -o agent main.go

# runtime image
FROM gcr.io/distroless/static-debian12:debug-nonroot

# install agent binary
WORKDIR /
COPY --from=builder /workspace/agent agent
26 changes: 26 additions & 0 deletions agent/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
VERSION ?= $(shell git describe --tags --always --dirty)

all: protoc agent

ensure:
go mod tidy

protoc:
@echo "Generating Go files"
cd pkg/proto && protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

test:
go test -covermode=atomic -coverprofile=coverage.out -v ./...

agent: protoc
@echo "Building agent"
go build -o pulumi-kubernetes-agent \
-ldflags "-X github.com/pulumi/pulumi-kubernetes-operator/agent/version.Version=${VERSION}" \
github.com/pulumi/pulumi-kubernetes-operator/agent

image: agent
docker build --build-arg="VERSION=$(VERSION)" -t pulumi/pulumi-kubernetes-agent:latest .

.PHONY: agent protoc image ensure test

83 changes: 83 additions & 0 deletions agent/cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Copyright © 2024 Pulumi Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"os"

"github.com/fluxcd/pkg/http/fetch"
"github.com/spf13/cobra"
"go.uber.org/zap"
)

const (
DefaultFluxRetries = 3
)

var (
TargetDir string
FluxUrl string
FluxDigest string
)

// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize a Pulumi workspace",
Long: `Initialize a working directory to contain project sources.
For Flux sources:
pulumi-kubernetes-agent init --flux-fetch-url URL
`,
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
log.Debugw("executing init command", "TargetDir", TargetDir)

err := os.MkdirAll(TargetDir, 0777)
if err != nil {
log.Errorw("fatal: unable to make target directory", zap.Error(err))
os.Exit(1)
}
log.Debugw("target directory created", "dir", TargetDir)

// fetch the configured flux source
if FluxUrl != "" {
// https://github.com/fluxcd/kustomize-controller/blob/a1a33f2adda783dd2a17234f5d8e84caca4e24e2/internal/controller/kustomization_controller.go#L328
fetcher := fetch.New(
fetch.WithRetries(DefaultFluxRetries),
fetch.WithHostnameOverwrite(os.Getenv("SOURCE_CONTROLLER_LOCALHOST")),
fetch.WithUntar())

log.Infow("flux source fetching", "url", FluxUrl, "digest", FluxDigest)
err := fetcher.FetchWithContext(ctx, FluxUrl, FluxDigest, TargetDir)
if err != nil {
log.Errorw("fatal: unable to fetch flux source", zap.Error(err))
os.Exit(2)
}
log.Infow("flux source fetched", "dir", TargetDir)
}
},
}

func init() {
rootCmd.AddCommand(initCmd)
initCmd.Flags().StringVarP(&TargetDir, "target-dir", "t", "", "The target directory to initialize")
initCmd.MarkFlagRequired("target-dir")

initCmd.Flags().StringVar(&FluxUrl, "flux-url", "", "Flux archive URL")
initCmd.Flags().StringVar(&FluxDigest, "flux-digest", "", "Flux digest")
initCmd.MarkFlagsRequiredTogether("flux-url", "flux-digest")
}
72 changes: 72 additions & 0 deletions agent/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright © 2024 Pulumi Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"os"

"github.com/spf13/cobra"
"go.uber.org/zap"
)

var verbose bool

// a command-specific logger
var log *zap.SugaredLogger

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "agent",
Short: "Pulumi Kubernetes Operator Agent",
Long: `Provides tooling and a gRPC service for the Pulumi Kubernetes Operator
to use to perform stack operations.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error

// initialize the global logger
zc := zap.NewDevelopmentConfig()
zc.DisableCaller = true
if !verbose {
zc.Level.SetLevel(zap.InfoLevel)
}
zapLog, err := zc.Build()
if err != nil {
return err
}
zap.ReplaceGlobals(zapLog)

// initialize a command-specific logger
log = zap.L().Named("cmd").Named(cmd.Name()).Sugar()
return nil
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
// ignore sync errors: https://github.com/uber-go/zap/pull/347
_ = zap.L().Sync()
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
}
Loading

0 comments on commit 5b5d8a7

Please sign in to comment.