Skip to content

Commit

Permalink
several refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
matheuscscp committed Jul 29, 2024
1 parent dfbf587 commit 4cc1ec3
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 241 deletions.
1 change: 0 additions & 1 deletion cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ func newServerCommand() *cobra.Command {
Node: node,
ServiceAccounts: serviceAccounts,
ServiceAccountTokens: serviceAccountTokens,
GoogleCredentialsConfig: googleCredentialsConfig,
MetricsRegistry: metricsRegistry,
DefaultNodeServiceAccount: defaultNodeServiceAccount,
})
Expand Down
4 changes: 2 additions & 2 deletions helm/gke-metadata-server/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.8.1
version: 0.8.2

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.8.1"
appVersion: "0.8.2"
41 changes: 18 additions & 23 deletions internal/googlecredentials/google_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,24 @@ func NewConfig(opts ConfigOptions) (*Config, error) {
return &Config{opts}, nil
}

func (c *Config) GetForFile(ctx context.Context, googleServiceAccountEmail, credFile string) (*google.Credentials, error) {
conf := c.Get(googleServiceAccountEmail, map[string]any{
"format": map[string]string{"type": "text"},
"file": credFile,
})
func (c *Config) Get(ctx context.Context, googleServiceAccountEmail, credFile string) (*google.Credentials, error) {
conf := map[string]any{
"type": "external_account",
"audience": c.WorkloadIdentityProviderAudience(),
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": map[string]any{
"format": map[string]string{"type": "text"},
"file": credFile,
},
"service_account_impersonation_url": fmt.Sprintf(
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
googleServiceAccountEmail),
"service_account_impersonation": map[string]any{
"token_lifetime_seconds": c.TokenExpirationSeconds(),
},
}

b, err := json.Marshal(conf)
if err != nil {
return nil, fmt.Errorf("error marshaling google credentials config to json: %w", err)
Expand All @@ -75,24 +88,6 @@ func (c *Config) GetForFile(ctx context.Context, googleServiceAccountEmail, cred
return creds, nil
}

func (c *Config) Get(googleServiceAccountEmail string, credSource map[string]any) any {
impersonationURL := fmt.Sprintf(
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken",
googleServiceAccountEmail)

return map[string]any{
"type": "external_account",
"audience": c.WorkloadIdentityProviderAudience(),
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": credSource,
"service_account_impersonation_url": impersonationURL,
"service_account_impersonation": map[string]any{
"token_lifetime_seconds": c.TokenExpirationSeconds(),
},
}
}

func (c *Config) WorkloadIdentityProviderAudience() string {
return fmt.Sprintf("//iam.googleapis.com/%s", c.opts.WorkloadIdentityProvider)
}
Expand Down
10 changes: 9 additions & 1 deletion internal/http/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func RespondError(w http.ResponseWriter, r *http.Request, statusCode int, err er
RespondJSON(w, r, statusCode, resp)

t0 := r.Context().Value(reqStartTimeContextKey{}).(time.Time) // let this panic if a time is not present
l = l.WithField("latency", time.Since(t0).String())
l = l.WithField("latency", LatencyLogFields(t0))
if statusCode < 500 {
l.Info("client error")
} else {
Expand Down Expand Up @@ -135,6 +135,14 @@ func ResponseLogFields(statusCode int) logrus.Fields {
}
}

func LatencyLogFields(t0 time.Time) logrus.Fields {
latency := time.Since(t0)
return logrus.Fields{
"string": latency.String(),
"nanos": latency.Nanoseconds(),
}
}

func StartTimeIntoRequest(r *http.Request, t0 time.Time) *http.Request {
return r.WithContext(context.WithValue(r.Context(), reqStartTimeContextKey{}, t0))
}
Expand Down
8 changes: 3 additions & 5 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"net/http/httptest"
"time"

"github.com/matheuscscp/gke-metadata-server/internal/googlecredentials"
pkghttp "github.com/matheuscscp/gke-metadata-server/internal/http"
"github.com/matheuscscp/gke-metadata-server/internal/logging"
"github.com/matheuscscp/gke-metadata-server/internal/metrics"
Expand Down Expand Up @@ -59,7 +58,6 @@ type (
Node node.Provider
ServiceAccounts serviceaccounts.Provider
ServiceAccountTokens serviceaccounttokens.Provider
GoogleCredentialsConfig *googlecredentials.Config
MetricsRegistry *prometheus.Registry
DefaultNodeServiceAccount *serviceaccounts.Reference
}
Expand Down Expand Up @@ -121,8 +119,8 @@ func New(ctx context.Context, opts ServerOptions) *Server {
statusRecorder := &pkghttp.StatusRecorder{ResponseWriter: w}
defer func() {
statusCode := fmt.Sprint(statusRecorder.StatusCode())
delta := time.Since(t0).Seconds() * 1000
latencyMillis.WithLabelValues(r.Method, r.URL.Path, statusCode).Observe(delta)
delta := time.Since(t0).Milliseconds()
latencyMillis.WithLabelValues(r.Method, r.URL.Path, statusCode).Observe(float64(delta))
}()

w = statusRecorder
Expand All @@ -149,7 +147,7 @@ func New(ctx context.Context, opts ServerOptions) *Server {
logging.
FromRequest(r).
WithFields(logrus.Fields{
"latency": time.Since(t0).String(),
"latency": pkghttp.LatencyLogFields(t0),
"http_response": pkghttp.ResponseLogFields(statusCode),
}).
Info("request")
Expand Down
134 changes: 134 additions & 0 deletions internal/serviceaccounts/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// MIT License
//
// Copyright (c) 2024 Matheus Pimenta
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

package serviceaccounts

import (
"fmt"
"regexp"
"strings"

"github.com/golang-jwt/jwt/v5"
corev1 "k8s.io/api/core/v1"
)

type Reference struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}

var (
ErrGKEAnnotationMissing = fmt.Errorf("gke annotation %q missing", gkeAnnotation)
ErrGKEAnnotationInvalid = fmt.Errorf("gke annotation %q has invalid google service account email", gkeAnnotation)
)

const (
gkeAnnotation = "iam.gke.io/gcp-service-account"

emulatorAPIGroup = "gke-metadata-server.matheuscscp.io"
serviceAccountName = emulatorAPIGroup + "/serviceAccountName"
serviceAccountNamespace = emulatorAPIGroup + "/serviceAccountNamespace"
)

var googleEmailRegex = regexp.MustCompile(`^[a-zA-Z0-9-]+@[a-zA-Z0-9-]+\.iam\.gserviceaccount\.com$`)

// ReferenceFromObject returns a ServiceAccount reference from a ServiceAccount object.
func ReferenceFromObject(sa *corev1.ServiceAccount) *Reference {
return &Reference{
Name: sa.Name,
Namespace: sa.Namespace,
}
}

// ReferenceFromPod returns a ServiceAccount reference from a Pod object.
func ReferenceFromPod(pod *corev1.Pod) *Reference {
return &Reference{
Name: pod.Spec.ServiceAccountName,
Namespace: pod.Namespace,
}
}

// ReferenceFromNode returns a ServiceAccount reference from the Node object annotations or labels.
// Annotations take precedence over labels because we encourage users to use annotations instead of
// labels in this case since. Labels are more impactful to etcd since they are indexed, and we don't
// need indexing here so we prefer annotations.
//
// The ServiceAccount reference is retrieved from the following pair of annotations or labels:
//
// gke-metadata-server.matheuscscp.io/serviceAccountName
//
// gke-metadata-server.matheuscscp.io/serviceAccountNamespace
//
// If the annotations or labels are not found, defaultRef is returned.
func ReferenceFromNode(node *corev1.Node, defaultRef *Reference) *Reference {
if ref := getServiceAccountReference(node.Annotations); ref != nil {
return ref
}
if ref := getServiceAccountReference(node.Labels); ref != nil {
return ref
}
return defaultRef
}

// ReferenceFromToken returns a ServiceAccount reference from a ServiceAccount Token.
func ReferenceFromToken(token string) *Reference {
tok, _, _ := jwt.NewParser().ParseUnverified(token, jwt.MapClaims{})
sub, _ := tok.Claims.GetSubject()
s := strings.Split(sub, ":") // system:serviceaccount:{namespace}:{name}
return &Reference{Namespace: s[2], Name: s[3]}
}

// GoogleEmail returns the Google service account email from the same annotation
// used in native GKE Workload Identity. The annotation is:
//
// iam.gke.io/gcp-service-account
func GoogleEmail(sa *corev1.ServiceAccount) (string, error) {
v, ok := sa.Annotations[gkeAnnotation]
if !ok {
return "", ErrGKEAnnotationMissing
}
if !googleEmailRegex.MatchString(v) {
return "", ErrGKEAnnotationInvalid
}
return v, nil
}

func getServiceAccountReference(m map[string]string) *Reference {
if m == nil {
return nil
}
name, ok := m[serviceAccountName]
if !ok {
return nil
}
namespace, ok := m[serviceAccountNamespace]
if !ok {
return nil
}
if name == "" || namespace == "" {
return nil
}
return &Reference{
Name: name,
Namespace: namespace,
}
}
103 changes: 0 additions & 103 deletions internal/serviceaccounts/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,113 +24,10 @@ package serviceaccounts

import (
"context"
"fmt"
"regexp"
"strings"

corev1 "k8s.io/api/core/v1"
)

type Reference struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}

type Provider interface {
Get(ctx context.Context, ref *Reference) (*corev1.ServiceAccount, error)
}

const (
gkeAnnotation = "iam.gke.io/gcp-service-account"

emulatorAPIGroup = "gke-metadata-server.matheuscscp.io"
tagServiceAccountName = emulatorAPIGroup + "/serviceAccountName"
tagServiceAccountNamespace = emulatorAPIGroup + "/serviceAccountNamespace"
)

var googleEmailRegex = regexp.MustCompile(`^[a-zA-Z0-9-]+@[a-zA-Z0-9-]+\.iam\.gserviceaccount\.com$`)

// ReferenceFromObject returns a ServiceAccount reference from a ServiceAccount object.
func ReferenceFromObject(sa *corev1.ServiceAccount) *Reference {
return &Reference{
Name: sa.Name,
Namespace: sa.Namespace,
}
}

// ReferenceFromPod returns a ServiceAccount reference from a Pod object.
func ReferenceFromPod(pod *corev1.Pod) *Reference {
return &Reference{
Name: pod.Spec.ServiceAccountName,
Namespace: pod.Namespace,
}
}

// ReferenceFromNode returns a ServiceAccount reference from the Node object annotations or labels.
// Annotations take precedence over labels because we encourage users to use annotations instead of
// labels in this case since. Labels are more impactful to etcd since they are indexed, and we don't
// need indexing here so we prefer annotations.
//
// The ServiceAccount reference is retrieved from the following pair of annotations or labels:
//
// gke-metadata-server.matheuscscp.io/serviceAccountName
//
// gke-metadata-server.matheuscscp.io/serviceAccountNamespace
//
// If the annotations or labels are not found, defaultRef is returned.
func ReferenceFromNode(node *corev1.Node, defaultRef *Reference) *Reference {
if ref := getServiceAccountReference(node.Annotations); ref != nil {
return ref
}
if ref := getServiceAccountReference(node.Labels); ref != nil {
return ref
}
return defaultRef
}

// GoogleEmail returns the Google service account email from the same annotation
// used in native GKE Workload Identity. The annotation is:
//
// iam.gke.io/gcp-service-account
func GoogleEmail(sa *corev1.ServiceAccount) (string, error) {
v, ok := sa.Annotations[gkeAnnotation]
if !ok {
return "", fmt.Errorf("annotation %s is missing for service account '%s/%s'",
gkeAnnotation, sa.Namespace, sa.Name)
}
v = strings.TrimSpace(v)
if !IsGoogleEmail(v) {
return "", fmt.Errorf("annotation %s value %q does not match pattern %q",
gkeAnnotation, v, googleEmailRegex.String())
}
return v, nil
}

// IsGoogleEmail returns true if the string is a valid Google service account email.
// The email must match the following pattern:
//
// ^[a-zA-Z0-9-]+@[a-zA-Z0-9-]+\.iam\.gserviceaccount\.com$
func IsGoogleEmail(s string) bool {
return googleEmailRegex.MatchString(s)
}

func getServiceAccountReference(m map[string]string) *Reference {
if m == nil {
return nil
}
name, ok := m[tagServiceAccountName]
if !ok {
return nil
}
namespace, ok := m[tagServiceAccountNamespace]
if !ok {
return nil
}
if name == "" || namespace == "" {
return nil
}
return &Reference{
Name: name,
Namespace: namespace,
}
}
Loading

0 comments on commit 4cc1ec3

Please sign in to comment.