Skip to content

Commit

Permalink
Initial lifecycle implementation
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
Co-authored-by: Nicolas Bender <nicolas.bender@sap.com>
Co-authored-by: Ralf Pannemans <ralf.pannemans@sap.com
Co-authored-by: Pavel Busko <pavel.busko@sap.com>
  • Loading branch information
3 people committed May 13, 2024
1 parent 34cf9e4 commit 1fef9d9
Show file tree
Hide file tree
Showing 38 changed files with 2,363 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/go.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Go

on:
# pull_request:
# branches: ["main"]
# push:
# branches: ["main"]
workflow_dispatch: {}

jobs:
# unit:
# runs-on: [self-hosted]
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-go@v5
# with:
# go-version: '1.22'
# - run: make test
# integration:
# runs-on: [self-hosted]
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-go@v5
# with:
# go-version: '1.22'
# - run: make integration
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
build:
GOARCH=amd64 GOOS=linux go build -o bin/ -ldflags "-s -w" code.cloudfoundry.org/cnbapplifecycle/cmd/builder code.cloudfoundry.org/cnbapplifecycle/cmd/launcher
test -f bin/diego-sshd || curl -sL https://storage.googleapis.com/cf-deployment-compiled-releases/diego-2.96.0-ubuntu-jammy-1.80-20240322-160011-008934012.tgz | tar -xzO ./compiled_packages/diego-sshd.tgz | tar -xzO ./diego-sshd > bin/diego-sshd && chmod +x bin/diego-sshd
test -f bin/healthcheck || curl -sL https://storage.googleapis.com/cf-deployment-compiled-releases/diego-2.96.0-ubuntu-jammy-1.80-20240322-160011-008934012.tgz | tar -xzO ./compiled_packages/healthcheck.tgz | tar -xzO ./healthcheck > bin/healthcheck && chmod +x bin/healthcheck

test:
go test -v -count=1 ./...

integration: build
INCLUDE_INTEGRATION_TESTS=true go test -v -count=1 ./integration --ginkgo.label-filter integration -ginkgo.v

package: build
tar czf bin/cnb_app_lifecycle.tgz -C bin builder launcher diego-sshd healthcheck

.PHONY: build test integration package
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
# cnbapplifecycle

Lifecycle that produces Cloud Foundry droplets using Cloud Native Buildpacks.

## Builder

| Flag(s) | Type | Description | Default |
| ---------------------- | --------- | ------------------- | ---------------------- |
| `-b`, `--buildpacks` | `strings` | buildpacks to use | |
| `-d`, `--droplet` | `string` | output droplet file | `/tmp/droplet` |
| `-l`, `--layers` | `string` | layers dir | `/home/vcap/layers` |
| `-r`, `--result` | `string` | result file | `/tmp/result.json` |
| `-w`, `--workspaceDir` | `string` | app workspace dir | `/home/vcap/workspace` |

### Metadata

Example

```json
{
"lifecycle_metadata": {
"buildpacks": [
{
"key": "paketo-buildpacks/node-engine",
"name": "paketo-buildpacks/node-engine@3.2.1",
"version": "3.2.1"
},
{
"key": "paketo-buildpacks/npm-install",
"name": "paketo-buildpacks/npm-install@1.5.0",
"version": "1.5.0"
},
{
"key": "paketo-buildpacks/node-start",
"name": "paketo-buildpacks/node-start@1.1.5",
"version": "1.1.5"
},
{
"key": "paketo-buildpacks/npm-start",
"name": "paketo-buildpacks/npm-start@1.0.17",
"version": "1.0.17"
}
]
},
"process_types": { "web": "sh /home/vcap/workspace/start.sh" },
"execution_metadata": "",
"lifecycle_type": "cnb"
}
```

## Launcher

Reads `config/metadata.toml` from `CNB_LAYERS_DIR` (default `/home/vcap/layers`) and launches the application using the Cloud Native Buildpacks [launcher](https://github.com/buildpacks/lifecycle).
259 changes: 259 additions & 0 deletions cmd/builder/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package cli

import (
"archive/tar"
"compress/gzip"
"context"
"encoding/json"
"os"
"path/filepath"

"code.cloudfoundry.org/cnbapplifecycle/pkg/archive"
"code.cloudfoundry.org/cnbapplifecycle/pkg/errors"
"code.cloudfoundry.org/cnbapplifecycle/pkg/log"
"code.cloudfoundry.org/cnbapplifecycle/pkg/staging"
"github.com/spf13/cobra"

"github.com/buildpacks/pack/pkg/blob"
"github.com/buildpacks/pack/pkg/image"

"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/buildpack"
"github.com/buildpacks/lifecycle/cache"
"github.com/buildpacks/lifecycle/cmd"
"github.com/buildpacks/lifecycle/launch"
"github.com/buildpacks/lifecycle/layers"
"github.com/buildpacks/lifecycle/phase"
"github.com/buildpacks/lifecycle/platform"
"github.com/buildpacks/lifecycle/platform/files"
)

const (
PlatformAPI = "0.13"
DefaultLayersPath = "/home/vcap/layers"
DefaultWorkspacePath = "/home/vcap/workspace"
)

var (
layersDir string
workspaceDir string
cacheDir string
cacheOutputFile string
result string
dropletFile string
buildpacks []string
envVarNames []string
)

var (
platformDir = filepath.Join(os.TempDir(), "platform")
buildpacksDir = filepath.Join(os.TempDir(), "buildpacks")
extensionsDir = filepath.Join(os.TempDir(), "extensions")
downloadCacheDir = filepath.Join(os.TempDir(), "download-cache")
)

func Execute() error {
return builderCmd.Execute()
}

func init() {
builderCmd.Flags().StringSliceVarP(&buildpacks, "buildpack", "b", nil, "buildpack(s) to use")
builderCmd.Flags().StringVarP(&dropletFile, "droplet", "d", "/tmp/droplet", "output droplet file")
builderCmd.Flags().StringVarP(&result, "result", "r", "/tmp/result.json", "result file")
builderCmd.Flags().StringVarP(&workspaceDir, "workspace-dir", "w", DefaultWorkspacePath, "app workspace dir")
builderCmd.Flags().StringVarP(&layersDir, "layers", "l", DefaultLayersPath, "layers dir")
builderCmd.Flags().StringSliceVarP(&envVarNames, "pass-env-var", "", nil, "environment variable(s) to pass to buildpacks")
builderCmd.Flags().StringVarP(&cacheDir, "cache-dir", "c", "/tmp/cache", "cache dir")
builderCmd.Flags().StringVarP(&cacheOutputFile, "cache-output", "", "/tmp/cache-output.tgz", "cache output")
_ = builderCmd.MarkFlagRequired("buildpack")
}

var builderCmd = &cobra.Command{
Use: "builder",
SilenceUsage: true,
RunE: func(cobraCmd *cobra.Command, cmdArgs []string) error {
platformAPI := api.MustParse(PlatformAPI)
inputs := platform.NewLifecycleInputs(platformAPI)

cmd.DisableColor(inputs.NoColor)
logger := log.NewLogger()
if err := logger.SetLevel(inputs.LogLevel); err != nil {
logger.Errorf("failed to set log level to %q, error: %s\n", inputs.LogLevel, err.Error())
return errors.ErrGenericBuild
}

for _, dir := range []string{layersDir, platformDir, buildpacksDir, extensionsDir, downloadCacheDir, cacheDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
logger.Errorf("failed to create %q, error: %s\n", dir, err.Error())
return errors.ErrGenericBuild
}
}

if err := staging.CreateEnvFiles(platformDir, envVarNames); err != nil {
logger.Errorf("failed to write env var files, error: %s\n", err.Error())
return errors.ErrGenericBuild
}

orderFile, err := os.Create(filepath.Join(buildpacksDir, "order.toml"))
if err != nil {
logger.Errorf("failed to create 'order.toml', error: %s\n", err.Error())
return errors.ErrGenericBuild
}

err = staging.DownloadBuildpacks(buildpacks, buildpacksDir, image.NewFetcher(logger, nil), blob.NewDownloader(logger, downloadCacheDir), orderFile, logger)
if err != nil {
logger.Errorf("failed to download buildpacks, error: %s\n", err.Error())
return errors.ErrDownloadingBuildpack
}

dirStore := platform.NewDirStore(buildpacksDir, extensionsDir)
detectorFactory := phase.NewHermeticFactory(
platformAPI,
&cmd.BuildpackAPIVerifier{},
files.Handler,
dirStore,
)

detector, err := detectorFactory.NewDetector(platform.LifecycleInputs{
PlatformAPI: platformAPI,
AppDir: workspaceDir,
BuildpacksDir: buildpacksDir,
LayersDir: layersDir,
OrderPath: orderFile.Name(),
PlatformDir: platformDir,
CacheDir: cacheDir,
UseDaemon: false,
}, logger)
if err != nil {
logger.Errorf("failed creating detector, error: %s\n", err.Error())
return errors.ErrGenericBuild
}

logger.Phase("DETECTING")
bGroup, plan, err := detector.Detect()
if err != nil {
logger.Errorf("failed 'detect' phase, error: %s\n", err.Error())
return errors.ErrDetecting
}

logger.Phase("RESTORING")
cache, err := cache.NewVolumeCache(cacheDir)
if err != nil {
logger.Errorf("failed to initialise cache, error: %s\n", err.Error())
return errors.ErrRestoring
}

restorer := phase.Restorer{
LayersDir: layersDir,
Logger: logger,
Buildpacks: bGroup.Group,
PlatformAPI: platformAPI,
}
if err := restorer.Restore(cache); err != nil {
logger.Errorf("failed to restore cached layers, error: %s\n", err.Error())
return errors.ErrRestoring
}

bldr := phase.Builder{
AppDir: workspaceDir,
LayersDir: layersDir,
PlatformDir: platformDir,
BuildExecutor: &buildpack.DefaultBuildExecutor{},
DirStore: dirStore,
Group: bGroup,
Logger: logger,
Out: os.Stdout,
Err: os.Stderr,
Plan: plan,
PlatformAPI: platformAPI,
AnalyzeMD: files.Analyzed{},
}

logger.Phase("BUILDING")
buildMeta, err := bldr.Build()
if err != nil {
logger.Errorf("failed 'build' phase, error: %s\n", err.Error())
return errors.ErrBuilding
}

if err := files.Handler.WriteBuildMetadata(launch.GetMetadataFilePath(layersDir), buildMeta); err != nil {
logger.Errorf("failed writing build metadata, error: %s\n", err.Error())
return errors.ErrGenericBuild
}

artifactsDir, err := os.MkdirTemp("", "lifecycle.exporter.layer")
if err != nil {
logger.Errorf("create temp directory for artifacts, error: %s\n", err.Error())
return errors.ErrGenericBuild
}

exporter := phase.Exporter{
Buildpacks: bGroup.Group,
Logger: logger,
PlatformAPI: platformAPI,
LayerFactory: &layers.Factory{
ArtifactsDir: artifactsDir,
UID: inputs.UID,
GID: inputs.GID,
Logger: logger,
Ctx: context.Background(),
},
}

logger.Phase("EXPORTING")
if err := exporter.Cache(layersDir, cache); err != nil {
logger.Errorf("failed to save cached layers, error: %s\n", err.Error())
return errors.ErrExporting
}

cacheOutFile, err := os.OpenFile(cacheOutputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
logger.Errorf("Failed to open %q, error: %s\n", cacheOutputFile, err.Error())
return errors.ErrGenericBuild
}
defer cacheOutFile.Close()

cgw := gzip.NewWriter(cacheOutFile)
defer cgw.Close()

if err := archive.FromDirectory(cacheDir, tar.NewWriter(cgw)); err != nil {
logger.Errorf("failed to save archive cache folder, error: %s\n", err.Error())
return errors.ErrExporting
}

resultData := staging.StagingResultFromMetadata(buildMeta)
resultBytes, err := json.Marshal(resultData)
if err != nil {
logger.Errorf("failed to marshal '/tmp/result.json', error: %s\n", err.Error())
return errors.ErrGenericBuild
}

if err := os.WriteFile(result, resultBytes, 0o644); err != nil {
logger.Errorf("failed to write '/tmp/result.json', error: %s\n", err.Error())
return errors.ErrGenericBuild
}
logger.Infof("result file saved to %q", result)

dropletOutFile, err := os.OpenFile(dropletFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
logger.Errorf("failed to open %q, error: %s\n", dropletFile, err.Error())
return errors.ErrGenericBuild
}
defer dropletOutFile.Close()

dgw := gzip.NewWriter(dropletOutFile)
defer dgw.Close()

if err := staging.RemoveBuildOnlyLayers(layersDir, bGroup.Group, logger); err != nil {
logger.Errorf("failed to remove build-only layers, error: %s\n", err.Error())
return errors.ErrExporting
}
if err := archive.FromDirectory(filepath.Dir(workspaceDir), tar.NewWriter(dgw)); err != nil {
logger.Errorf("failed 'export' phase, error: %s\n", err.Error())
return errors.ErrExporting
}
logger.Infof("droplet archive saved to %q", dropletFile)

return nil
},
}
16 changes: 16 additions & 0 deletions cmd/builder/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
"os"

"code.cloudfoundry.org/cnbapplifecycle/cmd/builder/cli"
"code.cloudfoundry.org/cnbapplifecycle/pkg/errors"
)

func main() {
err := cli.Execute()

if err != nil {
os.Exit(errors.ExitCodeFromError(err))
}
}
Loading

0 comments on commit 1fef9d9

Please sign in to comment.