Skip to content

Commit

Permalink
ingress-controller/config: support go durations in annotations (#1047)
Browse files Browse the repository at this point in the history
* ingress-controller/config: support go durations in annotations

* update

* update spelling
  • Loading branch information
calebdoxsey authored Oct 9, 2024
1 parent 6e3ac1b commit 743d129
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 102 deletions.
8 changes: 5 additions & 3 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ words:
- databroker
- deepcopy
- envtest
- filemgr
- mockgen
- oidc
- pomerium
- protobuf
- oidc
- protojson
- readyz
- sharedkey
- sslcert
- sslkey
- sslrootcert
- upsert
- uifs
- filemgr
- unmarshaled
- upsert
languageSettings:
- languageId: go
allowCompoundWords: false
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/iancoleman/strcase v0.3.0
github.com/martinlindhe/base36 v1.1.1
github.com/open-policy-agent/opa v0.69.0
github.com/pomerium/csrf v1.7.0
github.com/pomerium/pomerium v0.27.1-0.20241004190459-6f6186a67da2
github.com/rs/zerolog v1.33.0
github.com/sergi/go-diff v1.3.1
Expand Down Expand Up @@ -159,7 +160,6 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pomerium/csrf v1.7.0 // indirect
github.com/pomerium/datasource v0.18.2-0.20221108160055-c6134b5ed524 // indirect
github.com/pomerium/webauthn v0.0.0-20240603205124-0428df511172 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
Expand Down
44 changes: 0 additions & 44 deletions pomerium/ingress_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ package pomerium

import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"

envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
"github.com/open-policy-agent/opa/ast"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"

Expand Down Expand Up @@ -140,31 +136,6 @@ func removeKeyPrefix(src map[string]string, prefix string) (*keys, error) {
return &kv, nil
}

func toJSON(src map[string]string) ([]byte, error) {
dst := make(map[string]any, len(src))
for k, v := range src {
var out any
if err := yaml.Unmarshal([]byte(v), &out); err != nil {
return nil, fmt.Errorf("%s: %w", k, err)
}

// https://github.com/pomerium/pomerium/issues/4014 allow non-string values and convert them
if k == "set_request_headers" || k == "set_response_headers" {
if m, ok := out.(map[string]any); ok {
sm := map[string]string{}
for kk, vv := range m {
sm[kk] = fmt.Sprint(vv)
}
out = sm
}
}

dst[k] = out
}

return json.Marshal(dst)
}

// applyAnnotations applies ingress annotations to a route
func applyAnnotations(
r *pomerium.Route,
Expand Down Expand Up @@ -227,21 +198,6 @@ func unmarshalPolicyAnnotations(p *pomerium.Policy, kvs map[string]string) error
return nil
}

func unmarshalAnnotations(m protoreflect.ProtoMessage, kvs map[string]string) error {
if len(kvs) == 0 {
return nil
}

data, err := toJSON(kvs)
if err != nil {
return err
}

return (&protojson.UnmarshalOptions{
DiscardUnknown: false,
}).Unmarshal(data, m)
}

func applyTLSAnnotations(
r *pomerium.Route,
kvs map[string]string,
Expand Down
82 changes: 28 additions & 54 deletions pomerium/ingress_annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,42 +38,42 @@ func TestAnnotations(t *testing.T) {
ObjectMeta: v1.ObjectMeta{
Namespace: "test",
Annotations: map[string]string{
"a/allowed_users": `["a"]`,
"a/allowed_domains": `["a"]`,
"a/allowed_idp_claims": `{"key": ["val1", "val2"]}`,
"a/policy": testPPL,
"a/cors_allow_preflight": "true",
"a/allow_public_unauthenticated_access": "false",
"a/allow_any_authenticated_user": "false",
"a/timeout": `10s`,
"a/idle_timeout": `60s`,
"a/allow_websockets": "true",
"a/allow_public_unauthenticated_access": "false",
"a/allow_spdy": "true",
"a/set_request_headers": `{"a": "aaa"}`,
"a/set_request_headers_secret": `request_headers`,
"a/remove_request_headers": `["a"]`,
"a/set_response_headers": `{"disable": true}`,
"a/set_response_headers_secret": `response_headers`,
"a/rewrite_response_headers": `[{"header": "a", "prefix": "b", "value": "c"}]`,
"a/preserve_host_header": "true",
"a/host_rewrite": "rewrite",
"a/host_rewrite_header": "rewrite-header",
"a/allow_websockets": "true",
"a/allowed_domains": `["a"]`,
"a/allowed_idp_claims": `key: ["val1", "val2"]`,
"a/allowed_users": `["a"]`,
"a/cors_allow_preflight": "true",
"a/health_checks": `[{"timeout": "10s", "interval": "1m", "healthy_threshold": 1, "unhealthy_threshold": 2, "http_health_check": {"path": "/"}}]`,
"a/host_path_regex_rewrite_pattern": "rewrite-pattern",
"a/host_path_regex_rewrite_substitution": "rewrite-sub",
"a/pass_identity_headers": "true",
"a/health_checks": `[{"timeout": "10s", "interval": "60s", "healthy_threshold": 1, "unhealthy_threshold": 2, "http_health_check": {"path": "/"}}]`,
"a/tls_skip_verify": "true",
"a/tls_server_name": "my.server.name",
"a/tls_custom_ca_secret": "my_custom_ca_secret",
"a/tls_client_secret": "my_client_secret",
"a/tls_downstream_client_ca_secret": "my_downstream_client_ca_secret",
"a/secure_upstream": "true",
"a/host_rewrite_header": "rewrite-header",
"a/host_rewrite": "rewrite",
"a/idle_timeout": `60s`,
"a/kubernetes_service_account_token_secret": "k8s_token",
"a/lb_policy": "LEAST_REQUEST",
"a/least_request_lb_config": `{"choice_count":3,"active_request_bias":{"default_value":4,"runtime_key":"key"},"slow_start_config":{"slow_start_window":"3s","aggression":{"runtime_key":"key"}}}`,
"a/pass_identity_headers": "true",
"a/policy": testPPL,
"a/prefix_rewrite": "/",
"a/preserve_host_header": "true",
"a/regex_rewrite_pattern": `^/service/([^/]+)(/.*)$`,
"a/regex_rewrite_substitution": `\\2/instance/\\1`,
"a/kubernetes_service_account_token_secret": "k8s_token",
"a/remove_request_headers": `["a"]`,
"a/rewrite_response_headers": `[{"header": "a", "prefix": "b", "value": "c"}]`,
"a/secure_upstream": "true",
"a/set_request_headers_secret": `request_headers`,
"a/set_request_headers": `{"a": "aaa"}`,
"a/set_response_headers_secret": `response_headers`,
"a/set_response_headers": `{"disable": true}`,
"a/timeout": `2m`,
"a/tls_client_secret": "my_client_secret",
"a/tls_custom_ca_secret": "my_custom_ca_secret",
"a/tls_downstream_client_ca_secret": "my_downstream_client_ca_secret",
"a/tls_server_name": "my.server.name",
"a/tls_skip_verify": "true",
},
},
},
Expand Down Expand Up @@ -126,7 +126,7 @@ func TestAnnotations(t *testing.T) {
CorsAllowPreflight: true,
AllowPublicUnauthenticatedAccess: false,
AllowAnyAuthenticatedUser: false,
Timeout: durationpb.New(time.Second * 10),
Timeout: durationpb.New(time.Minute * 2),
IdleTimeout: durationpb.New(time.Minute),
AllowWebsockets: true,
AllowSpdy: true,
Expand Down Expand Up @@ -246,32 +246,6 @@ func TestMissingTlsAnnotationsSecretData(t *testing.T) {
}
}

func TestAnnotationsConversion(t *testing.T) {
for i, tc := range []struct {
in map[string]string
expect string
}{
{map[string]string{
"bool_param": "true",
"num_param": "10",
"txt_param": "text",
"allowed_idp_claims": `groups: ["admin", "audit"]`,
"allowed_groups": `["admin", "audit"]`,
}, `{
"bool_param": true,
"num_param": 10,
"txt_param": "text",
"allowed_groups": ["admin", "audit"],
"allowed_idp_claims": {"groups": ["admin", "audit"]}
}`},
} {
data, err := toJSON(tc.in)
if assert.NoError(t, err, i) {
assert.JSONEq(t, tc.expect, string(data), string(data))
}
}
}

func TestYaml(t *testing.T) {
for input, expect := range map[string]interface{}{
"10": 10,
Expand Down
113 changes: 113 additions & 0 deletions pomerium/proto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package pomerium

import (
"encoding/json"
"fmt"
"strings"
"time"

"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
"gopkg.in/yaml.v3"
)

func unmarshalAnnotations(dst proto.Message, kvs map[string]string) error {
// first convert the map[string]string to a map[string]any via yaml
src := make(map[string]any, len(kvs))
for k, v := range kvs {
var out any
if err := yaml.Unmarshal([]byte(v), &out); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
src[k] = out
}

// pre-process the json to handle custom formats
preprocessAnnotationMessage(dst.ProtoReflect().Descriptor(), src)

// marshal as json so it can be unmarshaled via protojson
data, err := json.Marshal(src)
if err != nil {
return err
}

return protojson.Unmarshal(data, dst)
}

func preprocessAnnotationMessage(md protoreflect.MessageDescriptor, data any) any {
switch md.FullName() {
case "google.protobuf.Duration":
// convert go duration strings into protojson duration strings
if v, ok := data.(string); ok {
return goDurationStringToProtoJSONDurationString(v)
}
default:
// preprocess all the fields
if v, ok := data.(map[string]any); ok {
fds := md.Fields()
for i := 0; i < fds.Len(); i++ {
fd := fds.Get(i)
name := string(fd.Name())
vv, ok := v[name]
if ok {
v[name] = preprocessAnnotationField(fd, vv)
}
}
return v
}
}
return data
}

func preprocessAnnotationField(fd protoreflect.FieldDescriptor, data any) any {
// if this is a repeated field, handle each of the field values separately
if fd.IsList() {
vs, ok := data.([]any)
if ok {
nvs := make([]any, len(vs))
for i, v := range vs {
nvs[i] = preprocessAnnotationFieldValue(fd, v)
}
return nvs
}
}

return preprocessAnnotationFieldValue(fd, data)
}

func preprocessAnnotationFieldValue(fd protoreflect.FieldDescriptor, data any) any {
// convert map[string]any -> map[string]string
if fd.IsMap() && fd.MapKey().Kind() == protoreflect.StringKind && fd.MapValue().Kind() == protoreflect.StringKind {
if v, ok := data.(map[string]any); ok {
m := make(map[string]string, len(v))
for k, vv := range v {
m[k] = fmt.Sprint(vv)
}
return m
}
}

switch fd.Kind() {
case protoreflect.MessageKind:
return preprocessAnnotationMessage(fd.Message(), data)
}

return data
}

func goDurationStringToProtoJSONDurationString(in string) string {
dur, err := time.ParseDuration(in)
if err != nil {
return in
}

bs, err := protojson.Marshal(durationpb.New(dur))
if err != nil {
return in
}

str := strings.Trim(string(bs), `"`)
return str
}

0 comments on commit 743d129

Please sign in to comment.