-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add different run modes for the TUF server, allow saving TUF keys as a secret #1214
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ package main | |
|
||
import ( | ||
"bytes" | ||
"context" | ||
"flag" | ||
"fmt" | ||
"net/http" | ||
|
@@ -33,13 +34,24 @@ import ( | |
"sigs.k8s.io/release-utils/version" | ||
) | ||
|
||
const ( | ||
modeInit = "init" | ||
modeInitAndServe = "init-and-serve" | ||
modeServe = "serve" | ||
modeInitNoOverwrite = "init-no-overwrite" | ||
) | ||
|
||
var ( | ||
dir = flag.String("file-dir", "/var/run/tuf-secrets", "Directory where all the files that need to be added to TUF root live. File names are used to as targets.") | ||
dir = flag.String("file-dir", "/var/run/tuf-secrets", "Directory where all the files that need to be added to TUF root live. File names are used to as targets.") | ||
targetDir = flag.String("target-dir", "", "Directory where TUF repository should be created/served from. Defaults to temporary directory.") | ||
mode = flag.String("mode", modeInitAndServe, "Run mode of the TUF server. One of: init, init-and-serve, serve, init-no-overwrite") | ||
// Name of the "secret" where we create two entries, one for: | ||
// root = Which holds 1.root.json | ||
// repository - Compressed repo, which has been tar/gzipped. | ||
secretName = flag.String("rootsecret", "tuf-root", "Name of the secret to create for the initial root file") | ||
noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment") | ||
// Name of the "secret" where we create one entry per key JSON definition as generated by TUF, e.g. "root.json", "timestamp.json", ... | ||
keysSecretName = flag.String("keyssecret", "", "Name of the secret to create for generated keys (keys won't be stored unless this is provided)") | ||
noK8s = flag.Bool("no-k8s", false, "Run in a non-k8s environment") | ||
) | ||
|
||
func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) { | ||
|
@@ -64,24 +76,20 @@ func getNamespaceAndClientset(noK8s bool) (string, *kubernetes.Clientset, error) | |
return ns, clientset, nil | ||
} | ||
|
||
func main() { | ||
flag.Parse() | ||
|
||
ctx := signals.NewContext() | ||
|
||
func initTUFRepo(ctx context.Context, certsDir, targetDir, repoSecretName, keysSecretName string) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracting this is a nice win anyway, though it's unfortunate that we have 4 string-type arguments next to each other. Oh well. |
||
versionInfo := version.GetVersionInfo() | ||
logging.FromContext(ctx).Infof("running create_repo Version: %s GitCommit: %s BuildDate: %s", versionInfo.GitVersion, versionInfo.GitCommit, versionInfo.BuildDate) | ||
|
||
ns, clientset, err := getNamespaceAndClientset(*noK8s) | ||
if err != nil { | ||
logging.FromContext(ctx).Panicf("Failed to get namespace and clientset: %v", err) | ||
return fmt.Errorf("failed to get namespace and clientset: %v", err) | ||
} | ||
|
||
tufFiles, err := os.ReadDir(*dir) | ||
trimDir := strings.TrimSuffix(certsDir, "/") | ||
tufFiles, err := os.ReadDir(trimDir) | ||
if err != nil { | ||
logging.FromContext(ctx).Fatalf("failed to read dir %s: %v", *dir, err) | ||
return fmt.Errorf("failed to read dir %s: %v", trimDir, err) | ||
} | ||
trimDir := strings.TrimSuffix(*dir, "/") | ||
files := map[string][]byte{} | ||
for _, file := range tufFiles { | ||
if !file.IsDir() { | ||
|
@@ -95,7 +103,7 @@ func main() { | |
fileName := fmt.Sprintf("%s/%s", trimDir, file.Name()) | ||
fileBytes, err := os.ReadFile(fileName) | ||
if err != nil { | ||
logging.FromContext(ctx).Fatalf("failed to read file %s/%s: %v", fileName, err) | ||
return fmt.Errorf("failed to read file %s: %v", fileName, err) | ||
} | ||
// If it's a TSA file, we need to split it into multiple TUF | ||
// targets. | ||
|
@@ -104,7 +112,7 @@ func main() { | |
|
||
certFiles, err := certs.SplitCertChain(fileBytes, "tsa") | ||
if err != nil { | ||
logging.FromContext(ctx).Fatalf("failed to parse %s/%s: %v", fileName, err) | ||
return fmt.Errorf("failed to parse %s: %v", fileName, err) | ||
} | ||
for k, v := range certFiles { | ||
logging.FromContext(ctx).Infof("Got tsa cert file %s", k) | ||
|
@@ -120,15 +128,16 @@ func main() { | |
// Create a new TUF root with the listed artifacts. | ||
local, dir, err := repo.CreateRepo(ctx, files) | ||
if err != nil { | ||
logging.FromContext(ctx).Panicf("Failed to create repo: %v", err) | ||
return fmt.Errorf("failed to create repo: %v", err) | ||
} | ||
|
||
meta, err := local.GetMeta() | ||
if err != nil { | ||
logging.FromContext(ctx).Panicf("Getting meta: %v", err) | ||
return fmt.Errorf("getting meta: %v", err) | ||
} | ||
rootJSON, ok := meta["root.json"] | ||
if !ok { | ||
logging.FromContext(ctx).Panicf("Getting root: %v", err) | ||
return fmt.Errorf("getting root: %v", err) | ||
} | ||
|
||
// Add the initial 1.root.json to secrets. | ||
|
@@ -140,26 +149,112 @@ func main() { | |
// worries here. | ||
var compressed bytes.Buffer | ||
if err := repo.CompressFS(os.DirFS(dir), &compressed, map[string]bool{"keys": true, "staged": true}); err != nil { | ||
logging.FromContext(ctx).Fatalf("Failed to compress the repo: %v", err) | ||
return fmt.Errorf("failed to compress the repo: %v", err) | ||
} | ||
data["repository"] = compressed.Bytes() | ||
|
||
if !*noK8s { | ||
nsSecret := clientset.CoreV1().Secrets(ns) | ||
if err := secret.ReconcileSecret(ctx, *secretName, ns, data, nsSecret); err != nil { | ||
logging.FromContext(ctx).Panicf("Failed to reconcile secret %s/%s: %v", ns, *secretName, err) | ||
if err := secret.ReconcileSecret(ctx, repoSecretName, ns, data, nsSecret); err != nil { | ||
return fmt.Errorf("failed to reconcile secret %s/%s: %v", ns, repoSecretName, err) | ||
} | ||
|
||
// If we should also store created keys in a secret, read all their files and save them in the secret | ||
if keysSecretName != "" { | ||
keyFiles, err := os.ReadDir(filepath.Join(dir, "keys")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't we pack the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We didn't. If you look at how this is done:
and how the |
||
if err != nil { | ||
return fmt.Errorf("failed to list keys directory %v", err) | ||
} | ||
dataKeys := map[string][]byte{} | ||
for _, keyFile := range keyFiles { | ||
if !strings.HasSuffix(keyFile.Name(), ".json") { | ||
continue | ||
} | ||
keyFilePath := filepath.Join(filepath.Join(dir, "keys", keyFile.Name())) | ||
content, err := os.ReadFile(keyFilePath) | ||
if err != nil { | ||
return fmt.Errorf("failed reading file %s: %v", keyFilePath, err) | ||
} | ||
dataKeys[keyFile.Name()] = content | ||
} | ||
if err := secret.ReconcileSecret(ctx, keysSecretName, ns, dataKeys, nsSecret); err != nil { | ||
return fmt.Errorf("failed to reconcile keys secret %s/%s: %v", ns, keysSecretName, err) | ||
} | ||
} | ||
} | ||
|
||
logging.FromContext(ctx).Infof("tuf repository was created in: %s, moving to %s", dir, targetDir) | ||
|
||
// Copy repository to the targetDir - until Go 1.23 which has os.CopyFS, we use | ||
// a quick hack where we uncompress the compressed repository to the targetDir | ||
repo.Uncompress(bytes.NewReader(data["repository"]), targetDir) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Right now, it seems like we're creating in a temp directory and then moving to the final destination. What about having the At that point, the code could also look like: if noK8s {
// We're done
return nil
} I suppose one advantage of the copy approach is that you won't end up with a half-initialized directory if you crash partway through.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There were two reasons why I didn't add the extra argument, even though we considered it:
|
||
return nil | ||
} | ||
|
||
func main() { | ||
flag.Parse() | ||
|
||
ctx := signals.NewContext() | ||
|
||
serve := false | ||
init := false | ||
overwrite := true | ||
|
||
switch *mode { | ||
case modeInit: | ||
init = true | ||
case modeInitAndServe: | ||
init = true | ||
serve = true | ||
Comment on lines
+206
to
+208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming there's no use case for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally don't have such a usecase right now and I always try to keep new stuff at minimum. If we ever see a need for it in the future, it will be easy to add it in a fully backwards compatible way. |
||
case modeInitNoOverwrite: | ||
init = true | ||
overwrite = false | ||
case modeServe: | ||
if *targetDir == "" { | ||
logging.FromContext(ctx).Fatalf("'targetDir' must be specified to use the 'serve' mode") | ||
} | ||
serve = true | ||
default: | ||
logging.FromContext(ctx).Fatalf("unknown mode %s", *mode) | ||
} | ||
|
||
if *targetDir == "" { | ||
newTmpDir, err := os.MkdirTemp(os.TempDir(), "tuf-serve") | ||
if err != nil { | ||
logging.FromContext(ctx).Fatalf("failed creating temporary directory in %s: %v", os.TempDir(), err) | ||
} | ||
*targetDir = newTmpDir | ||
} | ||
|
||
if init { | ||
repoExists := false | ||
// See if the TUF repository already exists; right now we only see if root.json exists, | ||
// but this could certainly be made much more sophisticated | ||
if _, err := os.Stat(filepath.Join(*targetDir, "repository", "root.json")); err == nil { | ||
repoExists = true | ||
} | ||
|
||
if repoExists && !overwrite { | ||
logging.FromContext(ctx).Infof("TUF repository already exists, skipping initialization...") | ||
} else { | ||
err := initTUFRepo(ctx, *dir, *targetDir, *secretName, *keysSecretName) | ||
if err != nil { | ||
logging.FromContext(ctx).Fatalf("%v", err) | ||
} | ||
logging.FromContext(ctx).Infof("tuf repository was created in: %s", *targetDir) | ||
} | ||
} | ||
|
||
// Serve the TUF repository. | ||
logging.FromContext(ctx).Infof("tuf repository was created in: %s", dir) | ||
serveDir := filepath.Join(dir, "repository") | ||
logging.FromContext(ctx).Infof("tuf repository was created in: %s serving tuf root at %s", dir, serveDir) | ||
fs := http.FileServer(http.Dir(serveDir)) | ||
http.Handle("/", fs) | ||
if serve { | ||
// Serve the TUF repository. | ||
serveDir := filepath.Join(*targetDir, "repository") | ||
logging.FromContext(ctx).Infof("serving tuf root at %s", serveDir) | ||
fs := http.FileServer(http.Dir(serveDir)) | ||
http.Handle("/", fs) | ||
|
||
/* #nosec G114 */ | ||
if err := http.ListenAndServe(":8080", nil); err != nil { //nolint: gosec | ||
panic(err) | ||
/* #nosec G114 */ | ||
if err := http.ListenAndServe(":8080", nil); err != nil { //nolint: gosec | ||
panic(err) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we were using Cobra, I'd suggest using subcommands, but I don't want to pull in that dependency randomly.