feat(main): Add additional envvars to configure Nixery

Previously the code had hardcoded paths to runtime data (the Nix
builder & web files), which have now been moved into configuration
options.

Additionally configuration for the application is now centralised in a
single config struct, an instance of which is passed around the
application.

This makes it possible to implement a wrapper in Nix that will
configure the runtime data locations automatically.
This commit is contained in:
Vincent Ambo 2019-07-23 22:48:16 +01:00
parent db606a2952
commit ca8a742315

81
main.go
View File

@ -32,11 +32,20 @@ import (
"cloud.google.com/go/storage"
)
// ManifestMediaType stores the Content-Type used for the manifest itself. This
// corresponds to the "Image Manifest V2, Schema 2" described on this page:
// config holds the Nixery configuration options.
type config struct {
bucket string // GCS bucket to cache & serve layers
builder string // Nix derivation for building images
web string // Static files to serve over HTTP
port string // Port on which to launch HTTP server
}
// ManifestMediaType is the Content-Type used for the manifest itself.
// This corresponds to the "Image Manifest V2, Schema 2" described on
// this page:
//
// https://docs.docker.com/registry/spec/manifest-v2-2/
const ManifestMediaType string = "application/vnd.docker.distribution.manifest.v2+json"
const manifestMediaType string = "application/vnd.docker.distribution.manifest.v2+json"
// Image represents the information necessary for building a container image. This can
// be either a list of package names (corresponding to keys in the nixpkgs set) or a
@ -102,16 +111,19 @@ func convenienceNames(packages []string) []string {
// Call out to Nix and request that an image be built. Nix will, upon success, return
// a manifest for the container image.
func buildImage(image *image, ctx *context.Context, bucket *storage.BucketHandle) ([]byte, error) {
// This file is made available at runtime via Blaze. See the `data` declaration in `BUILD`
nixPath := "experimental/users/tazjin/nixery/build-registry-image.nix"
func buildImage(ctx *context.Context, cfg *config, image *image, bucket *storage.BucketHandle) ([]byte, error) {
packages, err := json.Marshal(image.packages)
if err != nil {
return nil, err
}
cmd := exec.Command("nix-build", "--no-out-link", "--show-trace", "--argstr", "name", image.name, "--argstr", "packages", string(packages), nixPath)
cmd := exec.Command(
"nix-build",
"--no-out-link",
"--show-trace",
"--argstr", "name", image.name,
"--argstr", "packages", string(packages), cfg.builder,
)
outpipe, err := cmd.StdoutPipe()
if err != nil {
@ -206,11 +218,11 @@ func uploadLayer(ctx *context.Context, bucket *storage.BucketHandle, layer strin
// layerRedirect constructs the public URL of the layer object in the Cloud Storage bucket
// and redirects the client there.
//
// The Docker client is known to follow redirects, but this might not be true for all other
// registry clients.
func layerRedirect(w http.ResponseWriter, bucket string, digest string) {
log.Printf("Redirecting layer '%s' request to bucket '%s'\n", digest, bucket)
url := fmt.Sprintf("https://storage.googleapis.com/%s/layers/%s", bucket, digest)
// The Docker client is known to follow redirects, but this might not
// be true for all other registry clients.
func layerRedirect(w http.ResponseWriter, cfg *config, digest string) {
log.Printf("Redirecting layer '%s' request to bucket '%s'\n", digest, cfg.bucket)
url := fmt.Sprintf("https://storage.googleapis.com/%s/layers/%s", cfg.bucket, digest)
w.Header().Set("Location", url)
w.WriteHeader(303)
}
@ -219,15 +231,15 @@ func layerRedirect(w http.ResponseWriter, bucket string, digest string) {
// stored after Nix builds. Nixery does not directly serve layers to registry clients, instead it
// redirects them to the public URLs of the Cloud Storage bucket.
//
// The bucket is required for Nixery to function correctly, hence fatal errors are generated in case
// it fails to be set up correctly.
func prepareBucket(ctx *context.Context, bucket string) *storage.BucketHandle {
// The bucket is required for Nixery to function correctly, hence
// fatal errors are generated in case it fails to be set up correctly.
func prepareBucket(ctx *context.Context, cfg *config) *storage.BucketHandle {
client, err := storage.NewClient(*ctx)
if err != nil {
log.Fatalln("Failed to set up Cloud Storage client:", err)
}
bkt := client.Bucket(bucket)
bkt := client.Bucket(cfg.bucket)
if _, err := bkt.Attrs(*ctx); err != nil {
log.Fatalln("Could not access configured bucket", err)
@ -239,24 +251,29 @@ func prepareBucket(ctx *context.Context, bucket string) *storage.BucketHandle {
var manifestRegex = regexp.MustCompile(`^/v2/([\w|\-|\.|\_|\/]+)/manifests/(\w+)$`)
var layerRegex = regexp.MustCompile(`^/v2/([\w|\-|\.|\_|\/]+)/blobs/sha256:(\w+)$`)
func main() {
bucketName := os.Getenv("BUCKET")
if bucketName == "" {
log.Fatalln("GCS bucket for layer storage must be specified")
func getConfig(key, desc string) string {
value := os.Getenv(key)
if value == "" {
log.Fatalln(desc + " must be specified")
}
port := os.Getenv("PORT")
if port == "" {
port = "5726"
return value
}
func main() {
cfg := &config{
bucket: getConfig("BUCKET", "GCS bucket for layer storage"),
builder: getConfig("NIX_BUILDER", "Nix image builder code"),
web: getConfig("WEB_DIR", "Static web file dir"),
port: getConfig("PORT", "HTTP port"),
}
ctx := context.Background()
bucket := prepareBucket(&ctx, bucketName)
bucket := prepareBucket(&ctx, cfg)
log.Printf("Starting Kubernetes Nix controller on port %s\n", port)
log.Printf("Starting Kubernetes Nix controller on port %s\n", cfg.port)
log.Fatal(http.ListenAndServe(":"+port, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// When running on AppEngine, HTTP traffic should be redirected to HTTPS.
log.Fatal(http.ListenAndServe(":"+cfg.port, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//
// This is achieved here by enforcing HSTS (with a one week duration) on responses.
if r.Header.Get("X-Forwarded-Proto") == "http" && strings.Contains(r.Host, "appspot.com") {
@ -265,7 +282,7 @@ func main() {
// Serve an index page to anyone who visits the registry's base URL:
if r.RequestURI == "/" {
index, _ := ioutil.ReadFile("experimental/users/tazjin/nixery/index.html")
index, _ := ioutil.ReadFile(cfg.web + "/index.html")
w.Header().Add("Content-Type", "text/html")
w.Write(index)
return
@ -283,14 +300,14 @@ func main() {
imageName := manifestMatches[1]
log.Printf("Requesting manifest for image '%s'", imageName)
image := imageFromName(manifestMatches[1])
manifest, err := buildImage(&image, &ctx, bucket)
manifest, err := buildImage(&ctx, cfg, &image, bucket)
if err != nil {
log.Println("Failed to build image manifest", err)
return
}
w.Header().Add("Content-Type", ManifestMediaType)
w.Header().Add("Content-Type", manifestMediaType)
w.Write(manifest)
return
}
@ -300,7 +317,7 @@ func main() {
layerMatches := layerRegex.FindStringSubmatch(r.RequestURI)
if len(layerMatches) == 3 {
digest := layerMatches[2]
layerRedirect(w, bucketName, digest)
layerRedirect(w, cfg, digest)
return
}