diff --git a/README.md b/README.md index 26491355..6a7956cb 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ metadata: name: "example-nats-cluster" spec: size: 3 - version: "1.0.4" + version: "1.1.0" ' | kubectl apply -f - ``` @@ -108,7 +108,7 @@ metadata: spec: # Number of nodes in the cluster size: 3 - version: "1.0.4" + version: "1.1.0" tls: # Certificates to secure the NATS client connections: diff --git a/example/clients-auth-permissions.json b/example/clients-auth-permissions.json new file mode 100644 index 00000000..b9594c6f --- /dev/null +++ b/example/clients-auth-permissions.json @@ -0,0 +1,15 @@ +{ + "users": [ + { "username": "user1", "password": "user1secret" }, + { "username": "user2", "password": "user2secret", + "permissions": { + "publish": ["hello.*"], + "subscribe": ["hello.world"] + } + } + ], + "default_permissions": { + "publish": ["SANDBOX.*"], + "subscribe": ["PUBLIC.>"] + } +} diff --git a/example/example-nats-cluster-auth.yaml b/example/example-nats-cluster-auth.yaml new file mode 100644 index 00000000..9550b22e --- /dev/null +++ b/example/example-nats-cluster-auth.yaml @@ -0,0 +1,14 @@ +apiVersion: "nats.io/v1alpha2" +kind: "NatsCluster" +metadata: + name: "example-nats-auth" +spec: + size: 3 + version: "1.1.0" + + auth: + # Definition in JSON of the users permissions + clientsAuthSecret: "nats-clients-auth" + + # How long to wait for authentication + clientsAuthTimeout: 5 diff --git a/example/example-nats-cluster-tls.yaml b/example/example-nats-cluster-tls.yaml index 428aec3e..47e5620b 100644 --- a/example/example-nats-cluster-tls.yaml +++ b/example/example-nats-cluster-tls.yaml @@ -6,9 +6,9 @@ spec: # Number of nodes in the cluster size: 3 - # Must use 1.0.4 in order to allow waiting for the A record + # Must use 1.1.0 in order to allow waiting for the A record # from nodes in the cluster to be ready - version: "1.0.4" + version: "1.1.0" tls: # Certificates to secure the NATS client connections: diff --git a/example/example-nats-cluster.yaml b/example/example-nats-cluster.yaml index 3c886155..5ebb7777 100644 --- a/example/example-nats-cluster.yaml +++ b/example/example-nats-cluster.yaml @@ -4,4 +4,4 @@ metadata: name: "example-nats-1" spec: size: 3 - version: "1.0.4" + version: "1.1.0" diff --git a/pkg/conf/natsconf.go b/pkg/conf/natsconf.go index 42ba107f..fe3023c9 100644 --- a/pkg/conf/natsconf.go +++ b/pkg/conf/natsconf.go @@ -4,7 +4,6 @@ package natsconf import ( "bytes" "encoding/json" - "errors" ) type ServerConfig struct { @@ -39,35 +38,42 @@ type TLSConfig struct { } type AuthorizationConfig struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Token string `json:"token,omitempty"` - Timeout int `json:"timeout,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + Timeout int `json:"timeout,omitempty"` + Users []*User `json:"users,omitempty"` + DefaultPermissions *Permissions `json:"default_permissions,omitempty"` } -var ( - ErrInvalidConfig = errors.New("natsconf: cannot produce valid config") -) +type User struct { + User string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Permissions *Permissions `json:"permissions,omitempty"` +} + +// Permissions are the allowed subjects on a per +// publish or subscribe basis. +type Permissions struct { + Publish []string `json:"publish,omitempty"` + Subscribe []string `json:"subscribe,omitempty"` +} +// Marshal takes a server configuration and returns its +// JSON representation in bytes. func Marshal(conf *ServerConfig) ([]byte, error) { - js, err := json.MarshalIndent(conf, "", " ") + buf := &bytes.Buffer{} + encoder := json.NewEncoder(buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(conf) if err != nil { return nil, err } - if len(js) < 1 || len(js)-1 <= 1 { - return nil, ErrInvalidConfig + buf2 := &bytes.Buffer{} + err = json.Indent(buf2, buf.Bytes(), "", " ") + if err != nil { + return nil, err } - // Slice the initial and final brackets from the - // resulting JSON configuration so gnatsd config parsers - // almost treats it as valid config. - js = js[1:] - js = js[:len(js)-1] - - // Replacing all commas with line breaks still keeps - // arrays valid and makes the top level configuration - // be able to be parsed as gnatsd config. - result := bytes.Replace(js, []byte(","), []byte("\n"), -1) - - return result, nil + return buf2.Bytes(), nil } diff --git a/pkg/conf/natsconf_test.go b/pkg/conf/natsconf_test.go index 560e4da3..025363cf 100644 --- a/pkg/conf/natsconf_test.go +++ b/pkg/conf/natsconf_test.go @@ -13,31 +13,36 @@ func TestConfMarshal(t *testing.T) { }{ { input: &ServerConfig{}, - output: "", - err: ErrInvalidConfig, + output: "{}", + err: nil, }, { input: &ServerConfig{ HTTPPort: 8222, }, - output: `"http_port": 8222`, - err: nil, + output: `{ + "http_port": 8222 +}`, + err: nil, }, { input: &ServerConfig{ Port: 4222, }, - output: `"port": 4222`, - err: nil, + output: `{ + "port": 4222 +}`, + err: nil, }, { input: &ServerConfig{ Port: 4222, HTTPPort: 8222, }, - output: `"port": 4222 - - "http_port": 8222`, + output: `{ + "port": 4222, + "http_port": 8222 +}`, err: nil, }, { @@ -48,13 +53,13 @@ func TestConfMarshal(t *testing.T) { Port: 6222, }, }, - output: `"port": 4222 - - "http_port": 8222 - + output: `{ + "port": 4222, + "http_port": 8222, "cluster": { "port": 6222 - }`, + } +}`, err: nil, }, { @@ -70,21 +75,18 @@ func TestConfMarshal(t *testing.T) { }, }, }, - output: `"port": 4222 - - "http_port": 8222 - + output: `{ + "port": 4222, + "http_port": 8222, "cluster": { - "port": 6222 - + "port": 6222, "routes": [ - "nats://nats-1.default.svc:6222" - - "nats://nats-2.default.svc:6222" - + "nats://nats-1.default.svc:6222", + "nats://nats-2.default.svc:6222", "nats://nats-3.default.svc:6222" ] - }`, + } +}`, err: nil, }, { @@ -102,25 +104,20 @@ func TestConfMarshal(t *testing.T) { }, }, }, - output: `"port": 4222 - - "http_port": 8222 - + output: `{ + "port": 4222, + "http_port": 8222, "cluster": { - "port": 6222 - + "port": 6222, "routes": [ - "nats://nats-1.default.svc:6222" - - "nats://nats-2.default.svc:6222" - + "nats://nats-1.default.svc:6222", + "nats://nats-2.default.svc:6222", "nats://nats-3.default.svc:6222" ] - } - - "debug": true - - "trace": true`, + }, + "debug": true, + "trace": true +}`, err: nil, }, { @@ -141,29 +138,50 @@ func TestConfMarshal(t *testing.T) { }, }, }, - output: `"port": 4222 - - "http_port": 8222 - + output: `{ + "port": 4222, + "http_port": 8222, "cluster": { - "port": 6222 - + "port": 6222, "routes": [ - "nats://nats-1.default.svc:6222" - - "nats://nats-2.default.svc:6222" - + "nats://nats-1.default.svc:6222", + "nats://nats-2.default.svc:6222", "nats://nats-3.default.svc:6222" - ] - + ], "tls": { - "ca_file": "/etc/nats-tls/ca.pem" - - "cert_file": "/etc/nats-tls/server.pem" - + "ca_file": "/etc/nats-tls/ca.pem", + "cert_file": "/etc/nats-tls/server.pem", "key_file": "/etc/nats-tls/server-key.pem" } - }`, + } +}`, + err: nil, + }, + { + input: &ServerConfig{ + Port: 4222, + HTTPPort: 8222, + Authorization: &AuthorizationConfig{ + DefaultPermissions: &Permissions{ + Publish: []string{"PUBLISH.>"}, + Subscribe: []string{"PUBLISH.*"}, + }, + }, + }, + output: `{ + "port": 4222, + "http_port": 8222, + "authorization": { + "default_permissions": { + "publish": [ + "PUBLISH.>" + ], + "subscribe": [ + "PUBLISH.*" + ] + } + } +}`, err: nil, }, } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 0b827938..d25ba550 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -15,7 +15,7 @@ package constants const ( - DefaultNatsVersion = "1.0.4" + DefaultNatsVersion = "1.1.0" // ClientPort is the port for the clients. ClientPort = 4222 diff --git a/pkg/spec/cluster.go b/pkg/spec/cluster.go index 1d790b3c..1afc5c2e 100644 --- a/pkg/spec/cluster.go +++ b/pkg/spec/cluster.go @@ -70,7 +70,6 @@ type ClusterSpec struct { // The version must follow the [semver]( http://semver.org) format, for example "1.0.4". // Only NATS released versions are supported: https://github.com/nats-io/gnatsd/releases // - // If version is not set, default is "1.0.4". Version string `json:"version"` // Paused is to pause the control of the operator for the cluster. @@ -83,6 +82,9 @@ type ClusterSpec struct { // TLS is the configuration to secure the cluster. TLS *TLSConfig `json:"tls,omitempty"` + + // Auth is the configuration to set permissions for users. + Auth *AuthConfig `json:"auth,omitempty"` } // TLSConfig is the optional TLS configuration for the cluster. @@ -127,6 +129,13 @@ type PodPolicy struct { NatsEnv []v1.EnvVar `json:"natsEnv,omitempty"` } +// AuthConfig is the authorization configuration for +// user permissions in the cluster. +type AuthConfig struct { + ClientsAuthSecret string `json:"clientsAuthSecret,omitempty"` + ClientsAuthTimeout int `json:"clientsAuthTimeout,omitempty"` +} + func (c *ClusterSpec) Validate() error { if c.Pod != nil { for k := range c.Pod.Labels { diff --git a/pkg/spec/zz_generated.deepcopy.go b/pkg/spec/zz_generated.deepcopy.go index 5ae4e986..e5b51e02 100644 --- a/pkg/spec/zz_generated.deepcopy.go +++ b/pkg/spec/zz_generated.deepcopy.go @@ -23,6 +23,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthConfig) DeepCopyInto(out *AuthConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthConfig. +func (in *AuthConfig) DeepCopy() *AuthConfig { + if in == nil { + return nil + } + out := new(AuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterCondition) DeepCopyInto(out *ClusterCondition) { *out = *in @@ -60,6 +76,15 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { **out = **in } } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + if *in == nil { + *out = nil + } else { + *out = new(AuthConfig) + **out = **in + } + } return } diff --git a/pkg/util/kubernetes/kubernetes.go b/pkg/util/kubernetes/kubernetes.go index a42dcd17..35d7b3c5 100644 --- a/pkg/util/kubernetes/kubernetes.go +++ b/pkg/util/kubernetes/kubernetes.go @@ -146,6 +146,34 @@ func addTLSConfig(sconfig *natsconf.ServerConfig, cs spec.ClusterSpec) { } } +// addAuthConfig fills the Auth configuration to be used in config map. +func addAuthConfig(kubecli corev1client.CoreV1Interface, ns string, sconfig *natsconf.ServerConfig, cs spec.ClusterSpec) error { + if cs.Auth == nil { + return nil + } + if cs.Auth.ClientsAuthSecret != "" { + result, err := kubecli.Secrets(ns).Get(cs.Auth.ClientsAuthSecret, metav1.GetOptions{}) + if err != nil { + return err + } + + var clientAuth *natsconf.AuthorizationConfig + for _, v := range result.Data { + err := json.Unmarshal(v, &clientAuth) + if err != nil { + return err + } + if cs.Auth.ClientsAuthTimeout > 0 { + clientAuth.Timeout = cs.Auth.ClientsAuthTimeout + } + sconfig.Authorization = clientAuth + break + } + return nil + } + return nil +} + // CreateAndWaitPod is an util for testing. // We should eventually get rid of this in critical code path and move it to test util. func CreateAndWaitPod(kubecli corev1client.CoreV1Interface, ns string, pod *v1.Pod, timeout time.Duration) (*v1.Pod, error) { @@ -191,6 +219,10 @@ func CreateConfigMap(kubecli corev1client.CoreV1Interface, clusterName, ns strin }, } addTLSConfig(sconfig, cluster) + err := addAuthConfig(kubecli, ns, sconfig, cluster) + if err != nil { + return err + } rawConfig, err := natsconf.Marshal(sconfig) if err != nil { @@ -242,6 +274,10 @@ func UpdateConfigMap(kubecli corev1client.CoreV1Interface, clusterName, ns strin }, } addTLSConfig(sconfig, cluster) + err = addAuthConfig(kubecli, ns, sconfig, cluster) + if err != nil { + return err + } rawConfig, err := natsconf.Marshal(sconfig) if err != nil { diff --git a/test/operator/basic_test.go b/test/operator/basic_test.go index b5bbf000..c5fecdbf 100644 --- a/test/operator/basic_test.go +++ b/test/operator/basic_test.go @@ -95,7 +95,7 @@ func TestCreateConfigMap(t *testing.T) { }, Spec: spec.ClusterSpec{ Size: size, - Version: "1.0.4", + Version: "1.1.0", }, } _, err = cl.ncli.Create(ctx, cluster) diff --git a/version/version.go b/version/version.go index 2c90037d..6cfef85c 100644 --- a/version/version.go +++ b/version/version.go @@ -15,6 +15,6 @@ package version var ( - OperatorVersion = "0.2.0-v1alpha2+git" - GitSHA = "Not provided (use ./build instead of go build)" + OperatorVersion = "0.2.1-v1alpha2+git" + GitSHA = "Not provided" )