Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Organization type #11

Merged
merged 22 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ exclude_patterns:
- '**/*.d.ts'
- 'e2e/lib/'
- '**/zz_generated.deepcopy.go'
- '**/mock/'
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ crd*.yaml

# Go releaser
dist/

# apiserver-runtime
apiserver.local.config/
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ build.docker: $(BIN_FILENAME) ## Build the docker image
clean: ## Cleans up the generated resources
rm -rf dist/ cover.out $(BIN_FILENAME) || true

.PHONY: run
KUBECONFIG ?= ~/.kube/config
run: build ## Starts control api against the configured kuberentes cluster
$(BIN_FILENAME) --secure-port 9443 --kubeconfig $(KUBECONFIG) --authentication-kubeconfig $(KUBECONFIG) --authorization-kubeconfig $(KUBECONFIG)

.PHONY: local-env
local-env-setup: ## Setup local kind-based dev environment
$(localenv_make) setup
Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,60 @@ See the [local-env/README.md](./local-env/README.md) for more details on the loc
Please be aware that the productive deployment of the control-api may run on a different Kubernetes distribution than [kind].

[kind]: https://kind.sigs.k8s.io/


### Running the control-api locally

You can run the control-api locally against the currently configured Kubernetes cluster with

```bash
make run
```

To access the locally running API server you need to register it with the [kind]-based local environment.
You can do this by applying the following.

```yaml
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1.organization.appuio.io
ccremer marked this conversation as resolved.
Show resolved Hide resolved
spec:
insecureSkipTLSVerify: true
group: organization.appuio.io
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: apiserver
namespace: default
port: 9443
version: v1
---
apiVersion: v1
kind: Service
metadata:
name: apiserver
namespace: default
spec:
ports:
- port: 9443
protocol: TCP
targetPort: 9443
type: ExternalName
externalName: 172.21.0.1 # Change to host IP
```

The `externalName` needs to be changed to your specific host IP.
When running kind on Linux you can find it with

```bash
docker inspect <kind-container> | jq '.[0].NetworkSettings.Networks.kind.Gateway'
```

After that you should be able to access your (with `make run` running) API server with

```bash
kubectl get organizations
```

21 changes: 21 additions & 0 deletions apis/organization/v1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package v1 contains API Schema definitions for the control-api v1 API group
// +kubebuilder:object:generate=true
// +kubebuilder:skip
// +groupName=organization.appuio.io
package v1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "organization.appuio.io", Version: "v1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
131 changes: 131 additions & 0 deletions apis/organization/v1/organization_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package v1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
)

var (
// TypeKey is the label key to identify organization namespaces
TypeKey = "appuio.io/resource.type"
// OrgType is the label value to identify organization namespaces
OrgType = "organization"
// DisplayNameKey is the annotation key that stores the display name
DisplayNameKey = "organization.appuio.io/display-name"
)

// NewOrganizationFromNS returns an Organization based on the given namespace
// If the namespace does not represent an organization it will return nil
func NewOrganizationFromNS(ns *corev1.Namespace) *Organization {
if ns == nil || ns.Labels == nil || ns.Labels[TypeKey] != OrgType {
return nil
}
displayName := ""
if ns.Annotations != nil {
displayName = ns.Annotations[DisplayNameKey]
}
org := &Organization{
ObjectMeta: *ns.ObjectMeta.DeepCopy(),
Spec: OrganizationSpec{
DisplayName: displayName,
},
}
if org.Annotations != nil {
delete(org.Annotations, DisplayNameKey)
delete(org.Labels, TypeKey)
}
return org
}

// +kubebuilder:object:root=true

// Organization is a representation of an APPUiO Cloud Organization
type Organization struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// Data holds the cluster specific metadata.
Spec OrganizationSpec `json:"spec,omitempty"`
glrf marked this conversation as resolved.
Show resolved Hide resolved
}

// OrganizationSpec defines the desired state of the Organization
type OrganizationSpec struct {
// DisplayName is a human-friendly name
DisplayName string `json:"displayName,omitempty"`
}

// Organization needs to implement the builder resource interface
var _ resource.Object = &Organization{}
ccremer marked this conversation as resolved.
Show resolved Hide resolved

// GetObjectMeta returns the objects meta reference.
func (o *Organization) GetObjectMeta() *metav1.ObjectMeta {
return &o.ObjectMeta
}

// GetGroupVersionResource returns the GroupVersionResource for this resource.
// The resource should be the all lowercase and pluralized kind
func (o *Organization) GetGroupVersionResource() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: GroupVersion.Group,
Version: GroupVersion.Version,
Resource: "organizations",
}
}

// IsStorageVersion returns true if the object is also the internal version -- i.e. is the type defined for the API group or an alias to this object.
// If false, the resource is expected to implement MultiVersionObject interface.
func (o *Organization) IsStorageVersion() bool {
return true
}

// NamespaceScoped returns true if the object is namespaced
func (o *Organization) NamespaceScoped() bool {
return false
}

// New returns a new instance of the resource
func (o *Organization) New() runtime.Object {
return &Organization{}
}

// NewList return a new list instance of the resource
func (o *Organization) NewList() runtime.Object {
return &OrganizationList{}
}

// +kubebuilder:object:root=true

// OrganizationList contains a list of Organizations
type OrganizationList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []Organization `json:"items"`
}

// OrganizationList needs to implement the builder resource interface
var _ resource.ObjectList = &OrganizationList{}

// GetListMeta returns the list meta reference.
func (in *OrganizationList) GetListMeta() *metav1.ListMeta {
return &in.ListMeta
}

// ToNamespace translates an Organization to the underlying namespace representation
func (o *Organization) ToNamespace() *corev1.Namespace {
ns := &corev1.Namespace{
ObjectMeta: *o.ObjectMeta.DeepCopy(),
}
if ns.Labels == nil {
ns.Labels = map[string]string{}
}
if ns.Annotations == nil {
ns.Annotations = map[string]string{}
}
ns.Labels[TypeKey] = OrgType
ns.Annotations[DisplayNameKey] = o.Spec.DisplayName
return ns
}
156 changes: 156 additions & 0 deletions apis/organization/v1/organization_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package v1

import (
"testing"

"github.com/stretchr/testify/assert"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestNewOrganizationFromNS(t *testing.T) {
tests := map[string]struct {
namespace *corev1.Namespace
organization *Organization
}{
"GivenNil_ThenNil": {},
"GivenNonOrgNs_ThenNil": {
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
},
},
"GivenNonOrgNsWithLabel_ThenNil": {
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Labels: map[string]string{
"foo": "bar",
},
},
},
},
"GivenOrgNs_ThenOrg": {
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{
TypeKey: OrgType,
},
Annotations: map[string]string{
DisplayNameKey: "Foo Bar Inc.",
},
},
},
organization: &Organization{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{},
Annotations: map[string]string{},
},
Spec: OrganizationSpec{
DisplayName: "Foo Bar Inc.",
},
},
},
"GivenOrgNsWithLabelAndAnnot_ThenOrgWithLabelAndAnnot": {
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{
TypeKey: OrgType,
"foo": "bar",
},
Annotations: map[string]string{
DisplayNameKey: "Foo Bar Inc.",
"bar": "buzz",
},
},
},
organization: &Organization{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"bar": "buzz",
},
},
Spec: OrganizationSpec{
DisplayName: "Foo Bar Inc.",
},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.organization, NewOrganizationFromNS(tt.namespace))
})
}
}

func TestOrganization_ToNamespace(t *testing.T) {
tests := map[string]struct {
organization *Organization
namespace *corev1.Namespace
}{
"GivenOrg_ThenOrgNs": {
organization: &Organization{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
},
Spec: OrganizationSpec{
DisplayName: "Foo Bar Inc.",
},
},
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{
TypeKey: OrgType,
},
Annotations: map[string]string{
DisplayNameKey: "Foo Bar Inc.",
},
},
},
},
"GivenOrgWithLabelAndAnnot_ThenOrgNsWithLabelAndAnnot": {
organization: &Organization{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{
"foo": "bar",
},
Annotations: map[string]string{
"bar": "buzz",
},
},
Spec: OrganizationSpec{
DisplayName: "Foo Bar Inc.",
},
},
namespace: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "fooBar",
Labels: map[string]string{
TypeKey: OrgType,
"foo": "bar",
},
Annotations: map[string]string{
DisplayNameKey: "Foo Bar Inc.",
"bar": "buzz",
},
},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.namespace, tt.organization.ToNamespace())
})
}
}
Loading