mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-26 07:16:08 -05:00
AB#2032 Write IDs to disk and read when verifying (#212)
* AB#2032 Write IDs to disk and read when verifying * Update CHANGELOG.md * update changelog * update changelog * cli verify: prefer flag values * Rename fid file Co-authored-by: Thomas Tendyck <tt@edgeless.systems>
This commit is contained in:
parent
7cada2c9e8
commit
3177b2fdb7
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- GCP-native Kubernetes load balancing
|
||||
|
||||
### Changed
|
||||
- Create `constellation-id.json` when initializing the cluster to save the cluster's unique ID and the owner ID to disk. Verifying will read this file back to use the values for the verification. This is overriden by specifying the command line arguments.
|
||||
|
||||
### Removed
|
||||
|
||||
|
7
cli/internal/cmd/details.go
Normal file
7
cli/internal/cmd/details.go
Normal file
@ -0,0 +1,7 @@
|
||||
package cmd
|
||||
|
||||
type clusterIDFile struct {
|
||||
ClusterID string
|
||||
OwnerID string
|
||||
Endpoint string
|
||||
}
|
@ -267,6 +267,11 @@ func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler) er
|
||||
return fmt.Errorf("write kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
idFile := clusterIDFile{ClusterID: r.clusterID, OwnerID: r.ownerID, Endpoint: r.coordinatorPubIP}
|
||||
if err := fileHandler.WriteJSON(constants.IDsFileName, idFile, file.OptNone); err != nil {
|
||||
return fmt.Errorf("writing Constellation id file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(wr, "You can now connect to your cluster by executing:")
|
||||
fmt.Fprintf(wr, "\twg-quick up ./%s\n", constants.WGQuickConfigFilename)
|
||||
fmt.Fprintf(wr, "\texport KUBECONFIG=\"$PWD/%s\"\n", constants.AdminConfFilename)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -331,6 +332,13 @@ func TestWriteOutput(t *testing.T) {
|
||||
coordinatorPubIP: "baz-qq",
|
||||
kubeconfig: "foo-bar-baz-qq",
|
||||
}
|
||||
|
||||
expectedIdFile := clusterIDFile{
|
||||
Endpoint: result.coordinatorPubIP,
|
||||
ClusterID: result.clusterID,
|
||||
OwnerID: result.ownerID,
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
testFs := afero.NewMemMapFs()
|
||||
fileHandler := file.NewHandler(testFs)
|
||||
@ -340,11 +348,20 @@ func TestWriteOutput(t *testing.T) {
|
||||
assert.Contains(out.String(), result.clientVpnIP)
|
||||
assert.Contains(out.String(), result.coordinatorPubIP)
|
||||
assert.Contains(out.String(), result.coordinatorPubKey)
|
||||
assert.Contains(out.String(), result.clusterID)
|
||||
assert.Contains(out.String(), result.ownerID)
|
||||
|
||||
afs := afero.Afero{Fs: testFs}
|
||||
adminConf, err := afs.ReadFile(constants.AdminConfFilename)
|
||||
assert.NoError(err)
|
||||
assert.Equal(result.kubeconfig, string(adminConf))
|
||||
|
||||
idsFile, err := afs.ReadFile(constants.IDsFileName)
|
||||
assert.NoError(err)
|
||||
var testIdFile clusterIDFile
|
||||
err = json.Unmarshal(idsFile, &testIdFile)
|
||||
assert.NoError(err)
|
||||
assert.Equal(expectedIdFile, testIdFile)
|
||||
}
|
||||
|
||||
func TestIpsToEndpoints(t *testing.T) {
|
||||
|
@ -58,6 +58,10 @@ func validInstanceTypeForProvider(cmd *cobra.Command, insType string, provider c
|
||||
}
|
||||
|
||||
func validateEndpoint(endpoint string, defaultPort int) (string, error) {
|
||||
if endpoint == "" {
|
||||
return "", errors.New("endpoint is empty")
|
||||
}
|
||||
|
||||
_, _, err := net.SplitHostPort(endpoint)
|
||||
if err == nil {
|
||||
return endpoint, nil
|
||||
|
@ -65,6 +65,11 @@ func TestValidateEndpoint(t *testing.T) {
|
||||
defaultPort: 3,
|
||||
wantResult: "foo:3",
|
||||
},
|
||||
"empty endpoint": {
|
||||
endpoint: "",
|
||||
defaultPort: 3,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid endpoint": {
|
||||
endpoint: "foo:2:2",
|
||||
defaultPort: 3,
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
||||
"github.com/edgelesssys/constellation/cli/internal/cloudcmd"
|
||||
@ -25,7 +26,9 @@ func NewVerifyCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "verify {aws|azure|gcp}",
|
||||
Short: "Verify the confidential properties of a Constellation cluster",
|
||||
Long: "Verify the confidential properties of a Constellation cluster.",
|
||||
Long: `Verify the confidential properties of a Constellation cluster.
|
||||
|
||||
If arguments are not specified, values are read from ` + constants.IDsFileName + `.`,
|
||||
Args: cobra.MatchAll(
|
||||
cobra.ExactArgs(1),
|
||||
isCloudProvider(0),
|
||||
@ -35,8 +38,7 @@ func NewVerifyCmd() *cobra.Command {
|
||||
}
|
||||
cmd.Flags().String("owner-id", "", "verify using the owner identity derived from the master secret")
|
||||
cmd.Flags().String("unique-id", "", "verify using the unique cluster identity")
|
||||
cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT] (required)")
|
||||
must(cmd.MarkFlagRequired("node-endpoint"))
|
||||
cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT]")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -50,7 +52,7 @@ func runVerify(cmd *cobra.Command, args []string) error {
|
||||
func verify(
|
||||
cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, verifyClient verifyClient,
|
||||
) error {
|
||||
flags, err := parseVerifyFlags(cmd)
|
||||
flags, err := parseVerifyFlags(cmd, fileHandler)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -97,7 +99,11 @@ func verify(
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) {
|
||||
func parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handler) (verifyFlags, error) {
|
||||
configPath, err := cmd.Flags().GetString("config")
|
||||
if err != nil {
|
||||
return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err)
|
||||
}
|
||||
ownerID, err := cmd.Flags().GetString("owner-id")
|
||||
if err != nil {
|
||||
return verifyFlags{}, fmt.Errorf("parsing owner-id argument: %w", err)
|
||||
@ -106,22 +112,37 @@ func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) {
|
||||
if err != nil {
|
||||
return verifyFlags{}, fmt.Errorf("parsing unique-id argument: %w", err)
|
||||
}
|
||||
if ownerID == "" && clusterID == "" {
|
||||
return verifyFlags{}, errors.New("neither owner-id nor unique-id provided to verify the cluster")
|
||||
}
|
||||
|
||||
endpoint, err := cmd.Flags().GetString("node-endpoint")
|
||||
if err != nil {
|
||||
return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err)
|
||||
}
|
||||
endpoint, err = validateEndpoint(endpoint, constants.VerifyServiceNodePortGRPC)
|
||||
if err != nil {
|
||||
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
|
||||
|
||||
// Get empty values from ID file
|
||||
emptyEndpoint := endpoint == ""
|
||||
emptyIDs := ownerID == "" && clusterID == ""
|
||||
if emptyEndpoint || emptyIDs {
|
||||
if details, err := readIds(fileHandler); err == nil {
|
||||
if emptyEndpoint {
|
||||
cmd.Printf("Using endpoint from %q. Specify --node-endpoint to override this.\n", constants.IDsFileName)
|
||||
endpoint = details.Endpoint
|
||||
}
|
||||
if emptyIDs {
|
||||
cmd.Printf("Using IDs from %q. Specify --owner-id and/or --unique-id to override this.\n", constants.IDsFileName)
|
||||
ownerID = details.OwnerID
|
||||
clusterID = details.ClusterID
|
||||
}
|
||||
} else if !errors.Is(err, fs.ErrNotExist) {
|
||||
return verifyFlags{}, err
|
||||
}
|
||||
}
|
||||
|
||||
configPath, err := cmd.Flags().GetString("config")
|
||||
// Validate
|
||||
if ownerID == "" && clusterID == "" {
|
||||
return verifyFlags{}, errors.New("neither owner-id nor unique-id provided to verify the cluster")
|
||||
}
|
||||
endpoint, err = validateEndpoint(endpoint, constants.CoordinatorPort)
|
||||
if err != nil {
|
||||
return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err)
|
||||
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
|
||||
}
|
||||
|
||||
return verifyFlags{
|
||||
@ -139,6 +160,14 @@ type verifyFlags struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func readIds(fileHandler file.Handler) (clusterIDFile, error) {
|
||||
det := clusterIDFile{}
|
||||
if err := fileHandler.ReadJSON(constants.IDsFileName, &det); err != nil {
|
||||
return clusterIDFile{}, fmt.Errorf("reading cluster ids: %w", err)
|
||||
}
|
||||
return det, nil
|
||||
}
|
||||
|
||||
// verifyCompletion handles the completion of CLI arguments. It is frequently called
|
||||
// while the user types arguments of the command to suggest completion.
|
||||
func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
|
@ -61,11 +61,13 @@ func TestVerify(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
setupFs func(*require.Assertions) afero.Fs
|
||||
provider cloudprovider.Provider
|
||||
protoClient verifyClient
|
||||
protoClient *stubVerifyClient
|
||||
nodeEndpointFlag string
|
||||
configFlag string
|
||||
ownerIDFlag string
|
||||
clusterIDFlag string
|
||||
idFile *clusterIDFile
|
||||
wantEndpoint string
|
||||
wantErr bool
|
||||
}{
|
||||
"gcp": {
|
||||
@ -74,6 +76,7 @@ func TestVerify(t *testing.T) {
|
||||
nodeEndpointFlag: "192.0.2.1:1234",
|
||||
ownerIDFlag: zeroBase64,
|
||||
protoClient: &stubVerifyClient{},
|
||||
wantEndpoint: "192.0.2.1:1234",
|
||||
},
|
||||
"azure": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
@ -81,6 +84,7 @@ func TestVerify(t *testing.T) {
|
||||
nodeEndpointFlag: "192.0.2.1:1234",
|
||||
ownerIDFlag: zeroBase64,
|
||||
protoClient: &stubVerifyClient{},
|
||||
wantEndpoint: "192.0.2.1:1234",
|
||||
},
|
||||
"default port": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
@ -88,6 +92,31 @@ func TestVerify(t *testing.T) {
|
||||
nodeEndpointFlag: "192.0.2.1",
|
||||
ownerIDFlag: zeroBase64,
|
||||
protoClient: &stubVerifyClient{},
|
||||
wantEndpoint: "192.0.2.1:9000",
|
||||
},
|
||||
"endpoint not set": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
provider: cloudprovider.GCP,
|
||||
ownerIDFlag: zeroBase64,
|
||||
protoClient: &stubVerifyClient{},
|
||||
wantErr: true,
|
||||
},
|
||||
"endpoint from id file": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
provider: cloudprovider.GCP,
|
||||
ownerIDFlag: zeroBase64,
|
||||
protoClient: &stubVerifyClient{},
|
||||
idFile: &clusterIDFile{Endpoint: "192.0.2.1:1234"},
|
||||
wantEndpoint: "192.0.2.1:1234",
|
||||
},
|
||||
"override endpoint from details file": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
provider: cloudprovider.GCP,
|
||||
nodeEndpointFlag: "192.0.2.2:1234",
|
||||
ownerIDFlag: zeroBase64,
|
||||
protoClient: &stubVerifyClient{},
|
||||
idFile: &clusterIDFile{Endpoint: "192.0.2.1:1234"},
|
||||
wantEndpoint: "192.0.2.2:1234",
|
||||
},
|
||||
"invalid endpoint": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
@ -103,6 +132,14 @@ func TestVerify(t *testing.T) {
|
||||
nodeEndpointFlag: "192.0.2.1:1234",
|
||||
wantErr: true,
|
||||
},
|
||||
"use owner id from id file": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
provider: cloudprovider.GCP,
|
||||
nodeEndpointFlag: "192.0.2.1:1234",
|
||||
protoClient: &stubVerifyClient{},
|
||||
idFile: &clusterIDFile{OwnerID: zeroBase64},
|
||||
wantEndpoint: "192.0.2.1:1234",
|
||||
},
|
||||
"config file not existing": {
|
||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||
provider: cloudprovider.GCP,
|
||||
@ -153,6 +190,10 @@ func TestVerify(t *testing.T) {
|
||||
}
|
||||
fileHandler := file.NewHandler(tc.setupFs(require))
|
||||
|
||||
if tc.idFile != nil {
|
||||
require.NoError(fileHandler.WriteJSON(constants.IDsFileName, tc.idFile, file.OptNone))
|
||||
}
|
||||
|
||||
err := verify(cmd, tc.provider, fileHandler, tc.protoClient)
|
||||
|
||||
if tc.wantErr {
|
||||
@ -160,6 +201,7 @@ func TestVerify(t *testing.T) {
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Contains(out.String(), "OK")
|
||||
assert.Equal(tc.wantEndpoint, tc.protoClient.endpoint)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -284,9 +326,11 @@ func TestVerifyClient(t *testing.T) {
|
||||
|
||||
type stubVerifyClient struct {
|
||||
verifyErr error
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func (c *stubVerifyClient) Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error {
|
||||
c.endpoint = endpoint
|
||||
return c.verifyErr
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,7 @@ const (
|
||||
//
|
||||
|
||||
StateFilename = "constellation-state.json"
|
||||
IDsFileName = "constellation-id.json"
|
||||
ConfigFilename = "constellation-conf.yaml"
|
||||
DebugdConfigFilename = "cdbg-conf.yaml"
|
||||
AdminConfFilename = "constellation-admin.conf"
|
||||
|
Loading…
x
Reference in New Issue
Block a user