Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
14e0616
import operator
Intrpt Jun 4, 2024
ea0790e
import operator using github reference
Intrpt Jun 4, 2024
bbf5518
create crd
Intrpt Jun 6, 2024
0a700bb
introduced k8sApi
Intrpt Jun 6, 2024
d7d5597
gameService create & read resource from k8s
Intrpt Jun 6, 2024
8c7095a
load kubeconfig
Intrpt Jun 6, 2024
3df5c94
get kubeconfig from azure
Intrpt Jun 10, 2024
9e7bd15
split create client function
Intrpt Jun 10, 2024
4b03a01
imports
Intrpt Jun 10, 2024
f85a0f9
seperated updateGameUrl logic
Intrpt Jun 10, 2024
8494544
environment variables
Intrpt Jun 10, 2024
e0c2a9e
gitignore changes
Intrpt Jun 10, 2024
6588cef
Merge branch 'develop' into feature/51-create-game-crds-in-api
Intrpt Jun 10, 2024
f3e431e
Change build-push-action version
rieglerthomas Jun 10, 2024
71f8b8b
added new environment variables to readme
Intrpt Jun 10, 2024
39cb1d4
Change back build-push-action to v5
rieglerthomas Jun 10, 2024
ffc13c2
fix docker build
Intrpt Jun 10, 2024
be14494
dockerfile go version 1.20.14
Intrpt Jun 12, 2024
6c3d8a4
Merge remote-tracking branch 'origin/develop' into feature/51-create-…
Intrpt Jun 12, 2024
c6132db
removed unused import
Intrpt Jun 12, 2024
aa43c9c
dockerfile go version 1.20.12
Intrpt Jun 12, 2024
8e6beba
Dockerfile go version 1.22
Intrpt Jun 12, 2024
2e134b6
update dependencies
Intrpt Jun 12, 2024
a5fd60e
solved merge conflicts
Intrpt Jun 13, 2024
da29d04
fix missing dependencies
Intrpt Jun 13, 2024
9d78d43
fix problems after merge
Intrpt Jun 13, 2024
19cd577
missing err check
Intrpt Jun 13, 2024
35e9eea
removed Log.Fatal() in gameService.Delete
Intrpt Jun 13, 2024
8115487
moved azure api calls into separate file
stefancsukker Jun 13, 2024
b419497
Merge branch 'feature/51-create-game-crds-in-api' of https://github.c…
stefancsukker Jun 13, 2024
617aa5f
added missing import in main.go
stefancsukker Jun 13, 2024
671631e
converting multipart.fileheader to bytes instead of os.file
stefancsukker Jun 14, 2024
085311f
readme
Intrpt Jun 14, 2024
6c6aff6
AZURE_CLIENT_SECRET color
Intrpt Jun 14, 2024
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ api/.idea/
api/internal/
.idea/

api/secrets.txt
helm/*/charts
11 changes: 7 additions & 4 deletions api/.env
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ MYSQL_ROOT_PASSWORD="Root#123"
MYSQL_HOST="localhost"
MYSQL_PORT=3306

AZURE_CLIENT_ID=""
AZURE_TENANT_ID=""
AZURE_CLIENT_SECRET=""
AZURE_CLIENT_ID: "" #used by NewDefaultAzureCredential
AZURE_TENANT_ID: "" #used by NewDefaultAzureCredential
AZURE_CLIENT_SECRET: "" #used by NewDefaultAzureCredential
AZURE_STORAGE_ACCOUNT="indiegamestream0"
AZURE_CONTAINER_NAME="games"
AZURE_CONTAINER_NAME="games"
AZURE_AKS_CLUSTER_NAME: "" #To get kubeconfig
AZURERM_SUBSCRIPTION_ID: "" #To get kubeconfig
AZURERM_RESOURCE_GROUP_NAME: "" #To get kubeconfig
11 changes: 7 additions & 4 deletions api/.env.deployment
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ MYSQL_ROOT_PASSWORD="changeme" #TODO change me.
MYSQL_HOST="mysql"
MYSQL_PORT="3306"

AZURE_CLIENT_ID=""
AZURE_TENANT_ID=""
AZURE_CLIENT_SECRET=""
AZURE_CLIENT_ID: "" #used by NewDefaultAzureCredential
AZURE_TENANT_ID: "" #used by NewDefaultAzureCredential
AZURE_CLIENT_SECRET: "" #used by NewDefaultAzureCredential
AZURE_AKS_CLUSTER_NAME: "" #To get kubeconfig
AZURE_STORAGE_ACCOUNT="indiegamestream0"
AZURE_CONTAINER_NAME="games"
AZURE_CONTAINER_NAME="games"
AZURERM_SUBSCRIPTION_ID: "" #To get kubeconfig
AZURERM_RESOURCE_GROUP_NAME: "" #To get kubeconfig
24 changes: 23 additions & 1 deletion api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,26 @@ The docker image will use the following environment variables:
| MYSQL_DATABASE | "api" | |
| MYSQL_ROOT_USER | "root" | |
| <span style="color:red">MYSQL_ROOT_PASSWORD</span> | <span style="color:red">"changeme"</span> | |
If you use the docker image directly (without our provided docker-compose), you must specify them.
| OAUTH_CLIENT | | |
| AZURE_CLIENT_ID | | |
| AZURE_TENANT_ID | | |
| AZURE_STORAGE_ACCOUNT | | |
| <span style="color:red"> AZURE_CLIENT_SECRET </span> | | |
| AZURE_CONTAINER_NAME | | |
| AZURE_AKS_CLUSTER_NAME | | |
| AZURERM_SUBSCRIPTION_ID | | |
| AZURERM_RESOURCE_GROUP_NAME | | |


If you use the docker image directly (without our provided docker-compose), you must specify them.

The api will use a kubeconfig for talking to a Kubernetes API server.
If --kubeconfig is set, will use the kubeconfig file at that location.
Otherwise will assume running in cluster and use the cluster provided kubeconfig.
It also applies saner defaults for QPS and burst based on the Kubernetes controller manager defaults (20 QPS, 30 burst)

Config precedence:
* --kubeconfig flag pointing at a file
* KUBECONFIG environment variable pointing at a file
* In-cluster config if running in cluster
* $HOME/.kube/ config if exists.
59 changes: 59 additions & 0 deletions api/apis/azureApi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package apis

import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"io"
"mime/multipart"
"os"
)

type IAzureApi interface {
UploadGame(blobContainerName string, gameID string, fileHeader *multipart.FileHeader) (string, error)
DeleteGame(blobContainerName string, gameID string) error
}

func (g azureApi) UploadGame(blobContainerName string, gameID string, fileHeader *multipart.FileHeader) (string, error) {
ctx := context.Background()

file, err := fileHeader.Open()
if err != nil {
return "", err
}

gameBytes, err := io.ReadAll(file)
if err != nil {
return "", err
}

_, err = g.azure.UploadBuffer(ctx, blobContainerName, gameID, gameBytes, nil)
if err != nil {
return "", err
}

storageLocation := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", os.Getenv("AZURE_STORAGE_ACCOUNT"), blobContainerName, gameID)

return storageLocation, nil
}

func (g azureApi) DeleteGame(blobContainerName string, gameID string) error {
ctx := context.Background()

_, err := g.azure.DeleteBlob(ctx, blobContainerName, gameID, nil)
if err != nil {
return err
}

return nil
}

type azureApi struct {
azure *azblob.Client
}

func AzureService(azure *azblob.Client) IAzureApi {
return &azureApi{
azure: azure,
}
}
93 changes: 93 additions & 0 deletions api/apis/k8sApi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package apis

import (
"api/models"
"context"
"errors"
"github.com/google/uuid"
streamv1 "indiegamestream.com/indiegamestream/api/stream/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type IK8sApi interface {
DeployGame(game *models.Game) error
ReadGameUrl(gameId uuid.UUID) (string, error)
}

func (g k8sApi) DeployGame(game *models.Game) error {

//Definitions
ctx := context.Background()
key := typeNamespacedName(game.ID.String())

//Check if the custom resource is already existing
err := g.k8sClient.Get(ctx, key, &streamv1.Game{})
if err == nil {
return errors.New("resource is already created")
} else if !k8serrors.IsNotFound(err) {
return err
}

//Define the custom resource
resource, err := createAndVerifyGameResource(game)
if err != nil {
return err
}

return g.k8sClient.Create(ctx, resource)
}

func (g k8sApi) ReadGameUrl(gameId uuid.UUID) (string, error) {
key := typeNamespacedName(gameId.String())
resource := streamv1.Game{}

err := g.k8sClient.Get(context.Background(), key, &resource)
if err != nil {
return "", err
} else {
return resource.Status.URL, nil
}
}

func typeNamespacedName(resourceName string) types.NamespacedName {
return types.NamespacedName{
Name: resourceName,
Namespace: "default",
}
}

func createAndVerifyGameResource(game *models.Game) (*streamv1.Game, error) {
if game.ID == uuid.Nil {
return nil, errors.New("game id is not set")
}
if game.Title == "" {
return nil, errors.New("game title is not set")
}
if game.Url == "" {
return nil, errors.New("game url is not set")
}

return &streamv1.Game{
ObjectMeta: metav1.ObjectMeta{
Name: game.ID.String(),
Namespace: "default",
},
Spec: streamv1.GameSpec{
Name: game.Title,
ExecutableURL: game.Url,
},
}, nil
}

type k8sApi struct {
k8sClient client.Client
}

func K8sService(k8sClient client.Client) IK8sApi {
return &k8sApi{
k8sClient: k8sClient,
}
}
70 changes: 67 additions & 3 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"api/apis"
"api/controllers"
"api/repositories"
"api/scripts"
Expand All @@ -9,13 +10,17 @@ import (
"database/sql"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
"log"
"net/http"
"os"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func setupRouter(db *sql.DB, azClient *azblob.Client) *gin.Engine {
Expand All @@ -24,13 +29,20 @@ func setupRouter(db *sql.DB, azClient *azblob.Client) *gin.Engine {
//Cors
r.Use(CORSMiddleware())

//Setup Repositories
//Repositories
gamesRepository := repositories.GameRepository(db)
gamesService := services.GameService(gamesRepository, azClient)
gamesController := controllers.GameController(gamesService)

//Apis
k8sApi := apis.K8sService(k8sClient())
azureApi := apis.AzureService(azClient)

//Services
gamesService := services.GameService(gamesRepository, k8sApi, azureApi)
authService := services.AuthService()

//Controllers
gamesController := controllers.GameController(gamesService)

// Ping test
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
Expand Down Expand Up @@ -72,6 +84,58 @@ func setupDatabase() *sql.DB {
return db
}

func setupManagedClustersClient() *armcontainerservice.ManagedClustersClient {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
log.Fatalf("failed to obtain a credential: %v", err)
}
clientFactory, err := armcontainerservice.NewClientFactory(
os.Getenv("AZURERM_SUBSCRIPTION_ID"), cred, nil)
if err != nil {
log.Fatalf("failed to create client: %v", err)
}
return clientFactory.NewManagedClustersClient()
}

func getKubeConfig() armcontainerservice.ManagedClustersClientListClusterUserCredentialsResponse {
managedClustersClient := setupManagedClustersClient()

res, err := managedClustersClient.ListClusterUserCredentials(context.Background(),
os.Getenv("AZURERM_RESOURCE_GROUP_NAME"),
os.Getenv("AZURE_AKS_CLUSTER_NAME"),
&armcontainerservice.ManagedClustersClientListClusterUserCredentialsOptions{ServerFqdn: nil, Format: nil})
if err != nil {
log.Fatalf("failed to finish the request: %v", err)
}

return res
}

func k8sClient() client.Client {
kubeConfig := getKubeConfig()
if len(kubeConfig.Kubeconfigs) == 0 {
log.Fatalf("The kubeconfig request was successful but it's response body is empty")
}
if len(kubeConfig.Kubeconfigs) > 1 {
log.Println("WARNING: Multiple kube-config's have been found. The first one will be used.")
}

clientConfig, err := clientcmd.NewClientConfigFromBytes(kubeConfig.Kubeconfigs[0].Value)
if err != nil {
log.Fatalf("failed to load kube config: %v", err)
}
restConfig, err := clientConfig.ClientConfig()

k8sc, err := client.New(
restConfig,
client.Options{Scheme: scheme.Scheme},
)
if err != nil {
log.Fatal(err.Error())
}
return k8sc
}

func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
Expand Down
10 changes: 5 additions & 5 deletions api/controllers/gameController.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ func (g gameController) GetGameById(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
}
if game == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"message": "Game not found"})
return
}
if game.Owner != c.GetString("subject") {
log.Print(fmt.Printf("%s tried to access an resource of %s", c.GetString("subject"), game.Owner))
c.AbortWithStatusJSON(
http.StatusForbidden,
gin.H{"message": "You don't have permission to access this resource"})
return
}
if game == nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"message": "Game not found"})
return
}

//Map to dto
resultDto := dtos.GetGameByIdResponseBody{}
Expand Down Expand Up @@ -103,8 +103,8 @@ func (g gameController) UploadGame(c *gin.Context) {
return
}

//Save the game in the database and azure
game, err := g.service.Save(file, title, sub)

if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
Expand Down
Loading