Skip to content

Commit

Permalink
process-user-data: improve startup time
Browse files Browse the repository at this point in the history
the binary is currently importing a lot of libraries which have costly
init() procedures (around 8-10s at startup) this is due to sharing
some consts with the CAA modules. We can avoid that by inlining code
in process-user-data and moving shared consts into dedicated modules.

We can use a cpuid field to narrow down a hypervisor, so we don't have
to probe an IMDS endpoint while circling through the cloud provider,
delaying startup.

Signed-off-by: Magnus Kulke <magnuskulke@microsoft.com>
  • Loading branch information
mkulke committed Nov 21, 2024
1 parent 4dea914 commit 8253fc8
Show file tree
Hide file tree
Showing 16 changed files with 169 additions and 401 deletions.
3 changes: 2 additions & 1 deletion src/cloud-api-adaptor/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/vishvananda/netns v0.0.4
github.com/vmware/govmomi v0.33.1 // indirect
golang.org/x/sys v0.21.0
golang.org/x/sys v0.27.0
google.golang.org/grpc v1.61.2
gopkg.in/yaml.v2 v2.4.0
k8s.io/cri-api v0.27.1 // indirect
Expand All @@ -52,6 +52,7 @@ require (
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
github.com/docker/docker v25.0.6+incompatible
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/klauspost/cpuid/v2 v2.2.9
github.com/moby/sys/mountinfo v0.7.1
github.com/pelletier/go-toml/v2 v2.1.0
github.com/sirupsen/logrus v1.9.3
Expand Down
6 changes: 4 additions & 2 deletions src/cloud-api-adaptor/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand Down Expand Up @@ -701,8 +703,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
Expand Down
7 changes: 2 additions & 5 deletions src/cloud-api-adaptor/pkg/adaptor/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/adaptor/proxy"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/agent"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/forwarder"
. "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/paths"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/podnetwork"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/securecomms/wnssh"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/util"
Expand All @@ -35,10 +36,6 @@ import (

const (
SrcAuthfilePath = "/root/containers/auth.json"
AAConfigPath = "/run/peerpod/aa.toml"
AuthFilePath = "/run/peerpod/auth.json"
CDHConfigPath = "/run/peerpod/cdh.toml"
InitdataPath = "/run/peerpod/initdata"
Version = "0.0.0"
)

Expand Down Expand Up @@ -325,7 +322,7 @@ func (s *cloudService) CreateVM(ctx context.Context, req *pb.CreateVMRequest) (r
}

cloudConfig.WriteFiles = append(cloudConfig.WriteFiles, cloudinit.WriteFile{
Path: InitdataPath,
Path: InitDataPath,
Content: initdataStr,
})
}
Expand Down
7 changes: 7 additions & 0 deletions src/cloud-api-adaptor/pkg/initdata/initdata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package initdata

type InitData struct {
Algorithm string `toml:"algorithm"`
Version string `toml:"version"`
Data map[string]string `toml:"data,omitempty"`
}
11 changes: 11 additions & 0 deletions src/cloud-api-adaptor/pkg/paths/paths.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package paths

const (
AACfgPath = "/run/peerpod/aa.toml"
AuthFilePath = "/run/peerpod/auth.json"
CDHCfgPath = "/run/peerpod/cdh.toml"
InitDataPath = "/run/peerpod/initdata"
AgentCfgPath = "/run/peerpod/agent-config.toml"
ForwarderCfgPath = "/run/peerpod/daemon.json"
DockerUserDataPath = "/peerpod/userdata.json"
)
33 changes: 33 additions & 0 deletions src/cloud-api-adaptor/pkg/userdata/heuristics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package userdata

import (
"context"
"os"

"github.com/klauspost/cpuid/v2"
)

func isAzureVM() bool {
return cpuid.CPU.HypervisorVendorID == cpuid.MSVM
}

func isAWSVM(ctx context.Context) bool {
if cpuid.CPU.HypervisorVendorID != cpuid.KVM {
return false
}
_, err := imdsGet(ctx, AWSImdsUrl, false, nil)
return err == nil
}

func isGCPVM(ctx context.Context) bool {
if cpuid.CPU.HypervisorVendorID != cpuid.KVM {
return false
}
_, err := imdsGet(ctx, GcpImdsUrl, false, []kvPair{{"Metadata-Flavor", "Google"}})
return err == nil
}

func isDockerContainer() bool {
_, err := os.ReadFile("/.dockerenv")
return err == nil
}
63 changes: 63 additions & 0 deletions src/cloud-api-adaptor/pkg/userdata/imds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package userdata

import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
)

type kvPair struct {
k string
v string
}

func imdsGet(ctx context.Context, url string, b64 bool, headers []kvPair) ([]byte, error) {
// If url is empty then return empty string
if url == "" {
return nil, fmt.Errorf("url is empty")
}

// Create a new HTTP client
client := &http.Client{}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %s", err)

}

for _, header := range headers {
req.Header.Add(header.k, header.v)
}

// Send the request and retrieve the response
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %s", err)

}
defer resp.Body.Close()

// Check if the response was successful
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("endpoint %s returned != 200 status code: %s", url, resp.Status)
}

// Read the response body and return it as a string
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %s", err)
}

if !b64 {
return body, nil
}

decoded, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
return nil, fmt.Errorf("failed to decode b64 encoded userData: %s", err)
}
return decoded, nil
}
59 changes: 35 additions & 24 deletions src/cloud-api-adaptor/pkg/userdata/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,31 @@ import (
"time"

"github.com/avast/retry-go/v4"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/adaptor/cloud"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/agent"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/forwarder"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-providers/aws"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-providers/azure"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-providers/docker"
"github.com/confidential-containers/cloud-api-adaptor/src/cloud-providers/gcp"
toml "github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v2"

"github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/initdata"
. "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/paths"
)

const (
ConfigParent = "/run/peerpod"
DigestPath = "/run/peerpod/initdata.digest"
PolicyPath = "/run/peerpod/policy.rego"
// Ref: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
AWSImdsUrl = "http://169.254.169.254/latest/dynamic/instance-identity/document"
AWSUserDataImdsUrl = "http://169.254.169.254/latest/user-data"
// Ref: https://docs.microsoft.com/en-us/azure/virtual-machines/linux/instance-metadata-service
AzureImdsUrl = "http://169.254.169.254/metadata/instance/compute?api-version=2021-01-01"
AzureUserDataImdsUrl = "http://169.254.169.254/metadata/instance/compute/userData?api-version=2021-01-01&format=text"
// Ref: https://cloud.google.com/compute/docs/storing-retrieving-metadata
GcpImdsUrl = "http://metadata.google.internal/computeMetadata/v1/instance"
GcpUserDataImdsUrl = "http://metadata.google.internal/computeMetadata/v1/instance/attributes/user-data"
)

var logger = log.New(log.Writer(), "[userdata/provision] ", log.LstdFlags|log.Lmsgprefix)
var WriteFilesList = []string{cloud.AAConfigPath, cloud.CDHConfigPath, agent.ConfigFilePath, forwarder.DefaultConfigPath, cloud.AuthFilePath, cloud.InitdataPath}
var InitdDataFilesList = []string{cloud.AAConfigPath, cloud.CDHConfigPath, PolicyPath}
var WriteFilesList = []string{AACfgPath, CDHCfgPath, AgentCfgPath, ForwarderCfgPath, AuthFilePath, InitDataPath}
var InitdDataFilesList = []string{AACfgPath, CDHCfgPath, PolicyPath}

type Config struct {
fetchTimeout int
Expand All @@ -48,7 +53,7 @@ func NewConfig(fetchTimeout int) *Config {
return &Config{
fetchTimeout: fetchTimeout,
parentPath: ConfigParent,
initdataPath: cloud.InitdataPath,
initdataPath: InitDataPath,
digestPath: DigestPath,
writeFiles: WriteFilesList,
initdataFiles: InitdDataFilesList,
Expand Down Expand Up @@ -78,51 +83,57 @@ func (d DefaultRetry) GetRetryDelay() time.Duration {
type AzureUserDataProvider struct{ DefaultRetry }

func (a AzureUserDataProvider) GetUserData(ctx context.Context) ([]byte, error) {
url := azure.AzureUserDataImdsUrl
url := AzureUserDataImdsUrl
logger.Printf("provider: Azure, userDataUrl: %s\n", url)
return azure.GetUserData(ctx, url)
return imdsGet(ctx, url, true, []kvPair{{"Metadata", "true"}})
}

type AWSUserDataProvider struct{ DefaultRetry }

func (a AWSUserDataProvider) GetUserData(ctx context.Context) ([]byte, error) {
url := aws.AWSUserDataImdsUrl
url := AWSUserDataImdsUrl
logger.Printf("provider: AWS, userDataUrl: %s\n", url)
return aws.GetUserData(ctx, url)
return imdsGet(ctx, url, false, nil)
}

type GCPUserDataProvider struct{ DefaultRetry }

func (g GCPUserDataProvider) GetUserData(ctx context.Context) ([]byte, error) {
url := gcp.GcpUserDataImdsUrl
url := GcpUserDataImdsUrl
logger.Printf("provider: GCP, userDataUrl: %s\n", url)
return gcp.GetUserData(ctx, url)
return imdsGet(ctx, url, true, []kvPair{{"Metadata-Flavor", "Google"}})
}

type DockerUserDataProvider struct{ DefaultRetry }

func (a DockerUserDataProvider) GetUserData(ctx context.Context) ([]byte, error) {
url := docker.DockerUserDataUrl
logger.Printf("provider: Docker, userDataUrl: %s\n", url)
return docker.GetUserData(ctx, url)
path := DockerUserDataPath
logger.Printf("provider: Docker, userDataPath: %s\n", path)
userData, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read file: %s", err)
}

return userData, nil
}

func newProvider(ctx context.Context) (UserDataProvider, error) {

// This checks for the presence of a file and doesn't rely on http req like the
// azure, aws ones, thereby making it faster and hence checking this first
if docker.IsDocker(ctx) {
if isDockerContainer() {
return DockerUserDataProvider{}, nil
}
if azure.IsAzure(ctx) {

if isAzureVM() {
return AzureUserDataProvider{}, nil
}

if aws.IsAWS(ctx) {
if isAWSVM(ctx) {
return AWSUserDataProvider{}, nil
}

if gcp.IsGCP(ctx) {
if isGCPVM(ctx) {
return GCPUserDataProvider{}, nil
}

Expand Down Expand Up @@ -232,7 +243,7 @@ func extractInitdataAndHash(cfg *Config) error {
if err != nil {
return fmt.Errorf("Error base64 decode initdata: %w", err)
}
initdata := cloud.InitData{}
initdata := initdata.InitData{}
err = toml.Unmarshal(decodedBytes, &initdata)
if err != nil {
return fmt.Errorf("Error unmarshalling initdata: %w", err)
Expand Down
18 changes: 6 additions & 12 deletions src/cloud-api-adaptor/pkg/userdata/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
"strings"
"testing"
"time"

"github.com/confidential-containers/cloud-api-adaptor/src/cloud-providers/azure"
)

var testAgentConfig string = `server_addr = 'unix:///run/kata-containers/agent.sock'
Expand Down Expand Up @@ -226,7 +224,7 @@ func startTestServerPlainText() *httptest.Server {
}

// TestGetUserData tests the getUserData function
func TestGetUserData(t *testing.T) {
func TestIMDSGet(t *testing.T) {
// Start a temporary HTTP server for the test simulating
// the Azure metadata service
srv := startTestServer()
Expand All @@ -238,8 +236,7 @@ func TestGetUserData(t *testing.T) {
// Send request to srv.URL at path /metadata/instance/compute/userData

reqPath := srv.URL + "/metadata/instance/compute/userData"
// Call the getUserData function
userData, _ := azure.GetUserData(ctx, reqPath)
userData, _ := imdsGet(ctx, reqPath, true, nil)

// Check that the userData is not empty
if userData == nil {
Expand All @@ -248,15 +245,14 @@ func TestGetUserData(t *testing.T) {
}

// TestInvalidGetUserDataInvalidUrl tests the getUserData function with an invalid URL
func TestInvalidGetUserDataInvalidUrl(t *testing.T) {
func TestInvalidIMDSGetInvalidUrl(t *testing.T) {

// Create a context
ctx := context.Background()

// Send request to invalid URL
reqPath := "invalidURL"
// Call the getUserData function
userData, _ := azure.GetUserData(ctx, reqPath)
userData, _ := imdsGet(ctx, reqPath, true, nil)

// Check that the userData is empty
if userData != nil {
Expand All @@ -272,8 +268,7 @@ func TestInvalidGetUserDataEmptyUrl(t *testing.T) {

// Send request to empty URL
reqPath := ""
// Call the getUserData function
userData, _ := azure.GetUserData(ctx, reqPath)
userData, _ := imdsGet(ctx, reqPath, true, nil)

// Check that the userData is empty
if userData != nil {
Expand Down Expand Up @@ -553,8 +548,7 @@ func TestFailPlainTextUserData(t *testing.T) {
// Send request to srv.URL at path /metadata/instance/compute/userData

reqPath := srv.URL + "/metadata/instance/compute/userData"
// Call the getUserData function
userData, _ := azure.GetUserData(ctx, reqPath)
userData, _ := imdsGet(ctx, reqPath, true, nil)

// Check that the userData is empty. Since plain text userData is not supported
if userData != nil {
Expand Down
Loading

0 comments on commit 8253fc8

Please sign in to comment.