-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Service annotation reconciler (#277)
* various changes Signed-off-by: Jasmin Gacic <jasmin.gacic@gmail.com> * add upstream to x-kusk if not present Signed-off-by: Jasmin Gacic <jasmin.gacic@gmail.com> * removed some leftovers Signed-off-by: Jasmin Gacic <jasmin.gacic@gmail.com> * removed some leftovers Signed-off-by: Jasmin Gacic <jasmin.gacic@gmail.com> * addressing PR remarks Signed-off-by: Jasmin Gacic <jasmin.gacic@gmail.com> * parse url before making http request to fetch openapi spec. Remove else clauses in favour of guard clauses * doc for API autopilot Signed-off-by: Jasmin Gacic <jasmin.gacic@gmail.com> * Update docs/api_autopilot.md Co-authored-by: Kyle Hodgetts <kyle@kubeshop.io> * Update docs/api_autopilot.md Co-authored-by: Kyle Hodgetts <kyle@kubeshop.io> * Update docs/api_autopilot.md Co-authored-by: Kyle Hodgetts <kyle@kubeshop.io> * Update docs/api_autopilot.md Co-authored-by: Kyle Hodgetts <kyle@kubeshop.io> Co-authored-by: Kyle Hodgetts <kyle@kubeshop.io>
- Loading branch information
1 parent
6d404a4
commit 9b55a67
Showing
4 changed files
with
221 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# API autopilot | ||
|
||
Kusk gateway supports API auto deployment. | ||
|
||
Any REST API will typically consist of a Kubernetes pod(s) that is running the server and a Kubernetes service pointed to it. | ||
|
||
## It just works | ||
To expose a pre-existing REST API in the cluster, you would just have to execute this: | ||
|
||
```sh | ||
kubectl apply -f svc.yaml | ||
``` | ||
Or edit an existing Kubernetes service to add the annotation `kusk-gateway/openapi-url` with a URL to the location of your OpenAPI | ||
|
||
## Under the hood | ||
|
||
Let's explain what is going on. | ||
|
||
We added a convenience method that will allow users to easily expose their REST API through Kusk gateway by using the `kusk-gateway/openapi-url` annotation, and here is how. | ||
|
||
Assuming that the user has already set up a pod that is running REST API server code and the pod name is `todo-backend`: | ||
|
||
```yaml | ||
apiVersion: v1 | ||
kind: Service | ||
metadata: | ||
name: todo-backend | ||
annotations: | ||
kusk-gateway/openapi-url: https://gist.githubusercontent.com/jasmingacic/082849b29d0e06e5f018a66f4cd49ec3/raw/e91c94cc82e7591031399e0d8c563d28a62de460/openapi.yaml | ||
#NOTE: we need a sleeker URL for this | ||
spec: | ||
type: ClusterIP | ||
selector: | ||
app: todo-backend # aforementioned pod name | ||
ports: | ||
- port: 3000 | ||
targetPort: http | ||
``` | ||
Once the service is deployed to a cluster Kusk gateway will pick it up and create an API resource. | ||
Annotation `kusk-gateway/openapi-url` contains URL to OpenAPI spec for the user. Provided API may or may not contain `x-kusk` [extension](extension.md) so the reconciler will check if `x-kusk` extension is present and: | ||
* if not present controller will add it and point upstream to the newly created service | ||
```yaml | ||
x-kusk: | ||
upstream: | ||
name: todo-backend | ||
namespace: default | ||
``` | ||
* or if the extension is present it will check if it contains `upstream` property configured. If not it will add it to the extension otherwise it will take OpenAPI definition as is and create API resource. | ||
|
||
|
||
Upcoming features: | ||
- Refresh interval | ||
- Versioning | ||
- ... | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/* | ||
Copyright 2022. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package controllers | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
|
||
gateway "github.com/kubeshop/kusk-gateway/api/v1alpha1" | ||
|
||
"gopkg.in/yaml.v3" | ||
corev1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/log" | ||
) | ||
|
||
// ServiceReconciler reconciles a Pod object | ||
type ServiceReconciler struct { | ||
client.Client | ||
Scheme *runtime.Scheme | ||
} | ||
|
||
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete | ||
//+kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch | ||
//+kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update | ||
func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { | ||
l := log.FromContext(ctx).WithName("service-reconciler") | ||
|
||
l.Info("Reconciling changed Service resource", "changed", req.NamespacedName) | ||
|
||
svc := &corev1.Service{} | ||
if err := r.Client.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, svc); err != nil { | ||
if errors.IsNotFound(err) { | ||
return ctrl.Result{}, nil | ||
} | ||
return ctrl.Result{}, err | ||
} | ||
|
||
val, ok := svc.Annotations["kusk-gateway/openapi-url"] | ||
if !ok { | ||
return ctrl.Result{}, nil | ||
} | ||
|
||
l.Info(`Detected "kusk-gateway/openapi-url" annotation`, "found", val) | ||
|
||
openapi, err := getOpenAPIfromURL(svc.Annotations["kusk-gateway/openapi-url"]) | ||
if err != nil { | ||
return ctrl.Result{}, err | ||
} | ||
|
||
var yml map[string]interface{} | ||
err = yaml.Unmarshal(openapi, &yml) | ||
if err != nil { | ||
return ctrl.Result{}, err | ||
} | ||
|
||
service := map[string]interface{}{"service": map[string]interface{}{ | ||
"name": req.Name, | ||
"namespace": req.Namespace, | ||
"port": svc.Spec.Ports[0].Port, | ||
}} | ||
upstream := map[string]interface{}{ | ||
"upstream": service, | ||
} | ||
|
||
if _, ok := yml["x-kusk"]; !ok { | ||
yml["x-kusk"] = upstream | ||
} | ||
|
||
kusk := yml["x-kusk"] | ||
if xkusk, ok := kusk.(map[string]interface{}); ok { | ||
if _, contains := xkusk["upstream"]; !contains { | ||
xkusk["upstream"] = service | ||
} | ||
} | ||
|
||
yamlPayload, err := yaml.Marshal(yml) | ||
if err != nil { | ||
return ctrl.Result{}, err | ||
} | ||
|
||
gatewaySpec := gateway.APISpec{ | ||
Spec: string(yamlPayload), | ||
} | ||
|
||
api := &gateway.API{} | ||
if err := r.Client.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, api); err != nil && errors.IsNotFound(err) { | ||
//create | ||
api.Name = req.Name | ||
api.Namespace = req.Namespace | ||
api.Spec = gatewaySpec | ||
if err := r.Client.Create(ctx, api, &client.CreateOptions{}); err != nil { | ||
l.Error(err, "error occured while creating API") | ||
return ctrl.Result{}, err | ||
} | ||
|
||
return ctrl.Result{}, nil | ||
} | ||
|
||
api.Spec = gatewaySpec | ||
if err := r.Client.Update(ctx, api, &client.UpdateOptions{}); err != nil { | ||
l.Error(err, "error occured while updating API") | ||
return ctrl.Result{}, err | ||
|
||
} | ||
|
||
return ctrl.Result{}, nil | ||
} | ||
|
||
// SetupWithManager sets up the controller with the Manager. | ||
func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { | ||
return ctrl.NewControllerManagedBy(mgr). | ||
For(&corev1.Service{}). | ||
Complete(r) | ||
} | ||
|
||
func getOpenAPIfromURL(u string) ([]byte, error) { | ||
if _, err := url.Parse(u); err != nil { | ||
return nil, fmt.Errorf("invalid url %s: %w", u, err) | ||
} | ||
|
||
resp, err := http.Get(u) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
b, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return b, err | ||
} |