Compare commits

...

11 Commits

Author SHA1 Message Date
Florian Beisel 2b41bbdbb9
📝 Updates the Swagger Docs 2024-01-21 16:22:04 +01:00
Florian Beisel 1669a65512
Implement server.go as a more complete example
This change implements the category / rule Server which will showcase
storig arbirtray alongside the hostname which are not part of hostname
generation.
2024-01-21 16:20:21 +01:00
Florian Beisel 600fb0e997
🏗️ Change notebook.go to use new interface 2024-01-21 16:19:42 +01:00
Florian Beisel b681395287
🔊 Add logging to server.log when token generation fails 2024-01-21 16:17:35 +01:00
Florian Beisel 8563c80ffe
📄 Add license 2024-01-21 16:13:43 +01:00
Florian Beisel 7df43f2cf3
🐛 fixes a bug not using the JWT Key correctly 2024-01-21 16:06:36 +01:00
Florian Beisel f4b6197728
🏗️ Move calling Generate function to rules
This change moves the responsibility of calling the HostnameRule.Generate
function to the corresponding HostnameRule.{Insert,Update} functions
which will allow autonomy especially for the update functions on when
a hostname needs to be regenerated.
2024-01-21 16:00:33 +01:00
Florian Beisel df6a785ac4
📄 add license 2024-01-21 14:59:42 +01:00
Florian Beisel a871f165b7
🎨 Use os.readfile instead of ioutil 2024-01-21 14:58:27 +01:00
Florian Beisel c51a17993e
🎨 Refactor basic server structure
These changes refactor the router handling into its own package to keep
main.go clean. Also API handlers are here refactored to their corresponding
files.
2024-01-21 14:54:34 +01:00
Florian Beisel a4748d7619
♻️ Rework rules registration
This commit adds a rule registry as a central place to register new
rules. It will expose a Description and a factory to create a rule
object.
2024-01-21 14:45:58 +01:00
23 changed files with 1020 additions and 264 deletions

37
api/auth_handlers.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright 2024 Florian Beisel
//
// 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 api
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Helloworld godoc
//
// @Summary Check your authentication
// @Schemes
// @Description Checks whether the user is successfully authenticated
// @ID hello
// @Tags Authentication
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {string} Helloworld
// @Router /hello [get]
func Helloworld(g *gin.Context) {
g.JSON(http.StatusOK, "helloworld")
}

View File

@ -1,161 +0,0 @@
package api
import (
"errors"
"net/http"
"strings"
"git.beisel.it/florian/hostname-service/db"
"git.beisel.it/florian/hostname-service/models"
"git.beisel.it/florian/hostname-service/rules"
"github.com/gin-gonic/gin"
)
func getHostnameRuleByCategory(category string) (rules.HostnameRule, error) {
switch category {
case "notebook":
return &rules.NotebookRule{}, nil
// ... other categories
default:
return nil, errors.New("unknown category")
}
}
// CreateOrUpdateHostname handles the hostname generation
func CreateOrUpdateHostname(c *gin.Context, isUpdate bool) {
category := c.Param("category")
var oldHostname string
if isUpdate {
oldHostname = c.Param("oldhostname")
}
var params map[string]interface{}
if err := c.BindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameters"})
return
}
rule, err := getHostnameRuleByCategory(category)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hostname, paramsJSON, err := rule.Generate(params)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if isUpdate {
err = rule.Update(category, oldHostname, hostname, paramsJSON)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
} else {
err = rule.Insert(category, hostname, paramsJSON)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error processing hostname"})
return
}
response := models.SimpleHostnameResponse{Hostname: hostname}
c.JSON(http.StatusOK, response)
}
// @Summary Delete a hostname from the database
// @Description List all details for a given category
// @ID delete-hostnames-by-category-and-name
// @Produce json
// @Param category path string true "Category of the hostname"
// @Param hostname path string true "Hostname to delete"
// @Success 200 {object} models.SimpleHostnameResponse "Hostname"
// @Security Bearer
// @Tags Manipulate existing Hostnames
// @Router /{category}/{hostname} [delete]
func DeleteHostname(c *gin.Context) {
category := c.Param("category")
hostname := c.Param("hostname")
err := db.DeleteHostname(category, hostname)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
response := models.SimpleHostnameResponse{Hostname: hostname}
c.JSON(http.StatusOK, response)
}
// @Summary Return a list of hosts and their details filtered by category
// @Description List all details for a given category
// @ID list-hostnames-by-category
// @Produce json
// @Param category path string true "Category of the hostname"
// @Success 200 {array} models.Hostname "An array of responses"
// @Security Bearer
// @Tags Querying Hostnames
// @Router /{category} [get]
func ListHostnamesByCategory(c *gin.Context) {
category := c.Param("category")
hostnames, err := db.GetHostnamesByCategory(category)
if err != nil {
if strings.Contains(err.Error(), "no rows found") {
c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostnames"})
return
}
c.JSON(http.StatusOK, hostnames)
}
// @Summary Return a single hostname by Category and Name
// @Description Return details for a single hostname identified by its category
// @ID get-hostname-by-category-and-name
// @Produce json
// @Param category path string true "Category of the hostname"
// @Param hostname path string true "Category of the hostname"
// @Success 200 {object} models.Hostname "A single response object"
// @Security Bearer
// @Tags Querying Hostnames
// @Router /{category}/{hostname} [get]
func GetHostnameByCategoryAndName(c *gin.Context) {
category := c.Param("category")
hostname := c.Param("hostname")
hostinfo, err := db.GetHostnameByCategoryAndName(category, hostname)
if err != nil {
if strings.Contains(err.Error(), "no rows found") {
c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostname"})
return
}
c.JSON(http.StatusOK, hostinfo)
}
// Helloworld godoc
//
// @Summary Check your authentication
// @Schemes
// @Description Checks whether the user is successfully authenticated
// @ID hello
// @Tags Authentication
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {string} Helloworld
// @Router /hello [get]
func Helloworld(g *gin.Context) {
g.JSON(http.StatusOK, "helloworld")
}

137
api/hostname_handlers.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright 2024 Florian Beisel
//
// 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 api
import (
"net/http"
"strings"
"git.beisel.it/florian/hostname-service/db"
"git.beisel.it/florian/hostname-service/models"
"github.com/gin-gonic/gin"
)
// @Summary Return a single hostname by Category and Name
// @Description Return details for a single hostname identified by its category
// @ID get-hostname-by-category-and-name
// @Produce json
// @Param category path string true "Category of the hostname"
// @Param hostname path string true "Category of the hostname"
// @Success 200 {object} models.Hostname "A single response object"
// @Security Bearer
// @Tags Querying Hostnames
// @Router /{category}/{hostname} [get]
func GetHostnameByCategoryAndName(c *gin.Context) {
category := c.Param("category")
hostname := c.Param("hostname")
hostinfo, err := db.GetHostnameByCategoryAndName(category, hostname)
if err != nil {
if strings.Contains(err.Error(), "no rows found") {
c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostname"})
return
}
c.JSON(http.StatusOK, hostinfo)
}
// @Summary Return a list of hosts and their details filtered by category
// @Description List all details for a given category
// @ID list-hostnames-by-category
// @Produce json
// @Param category path string true "Category of the hostname"
// @Success 200 {array} models.Hostname "An array of responses"
// @Security Bearer
// @Tags Querying Hostnames
// @Router /{category} [get]
func ListHostnamesByCategory(c *gin.Context) {
category := c.Param("category")
hostnames, err := db.GetHostnamesByCategory(category)
if err != nil {
if strings.Contains(err.Error(), "no rows found") {
c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostnames"})
return
}
c.JSON(http.StatusOK, hostnames)
}
// @Summary Delete a hostname from the database
// @Description List all details for a given category
// @ID delete-hostnames-by-category-and-name
// @Produce json
// @Param category path string true "Category of the hostname"
// @Param hostname path string true "Hostname to delete"
// @Success 200 {object} models.SimpleHostnameResponse "Hostname"
// @Security Bearer
// @Tags Manipulate existing Hostnames
// @Router /{category}/{hostname} [delete]
func DeleteHostname(c *gin.Context) {
category := c.Param("category")
hostname := c.Param("hostname")
err := db.DeleteHostname(category, hostname)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
response := models.SimpleHostnameResponse{Hostname: hostname}
c.JSON(http.StatusOK, response)
}
func CreateOrUpdateHostname(c *gin.Context, isUpdate bool) {
category := c.Param("category")
var oldHostname string
if isUpdate {
oldHostname = c.Param("oldhostname")
}
var params map[string]interface{}
if err := c.BindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameters"})
return
}
rule, err := getHostnameRuleByCategory(category)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var hostname string
if isUpdate {
hostname, err = rule.Update(category, oldHostname, params)
} else {
hostname, err = rule.Insert(category, params)
}
if err != nil {
if isUpdate && strings.Contains(err.Error(), "does not exist") {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return
}
response := models.SimpleHostnameResponse{Hostname: hostname}
c.JSON(http.StatusOK, response)
}

38
api/rule_handlers.go Normal file
View File

@ -0,0 +1,38 @@
// Copyright 2024 Florian Beisel
//
// 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 api
import (
"errors"
"net/http"
"git.beisel.it/florian/hostname-service/rules"
"github.com/gin-gonic/gin"
)
func ListAvailableRules(c *gin.Context) {
descriptions := make(map[string]string)
for category, descriptor := range rules.RulesRegistry {
descriptions[category] = descriptor.Description
}
c.JSON(http.StatusOK, descriptions)
}
func getHostnameRuleByCategory(category string) (rules.HostnameRule, error) {
if descriptor, exists := rules.RulesRegistry[category]; exists {
return descriptor.Factory(), nil
}
return nil, errors.New("unknown category")
}

View File

@ -1,3 +1,17 @@
// Copyright 2024 Florian Beisel
//
// 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 auth package auth
import ( import (
@ -8,14 +22,15 @@ import (
) )
func GenerateToken(username string) (string, error) { func GenerateToken(username string) (string, error) {
jwtKeyBytes := []byte(config.GlobalConfig.JwtKey)
expirationTime := time.Now().Add(1 * time.Hour) expirationTime := time.Now().Add(1 * time.Hour)
claims := &jwt.StandardClaims{ claims := &jwt.StandardClaims{
Subject: username, Subject: username,
ExpiresAt: expirationTime.Unix(), ExpiresAt: expirationTime.Unix(),
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(config.GlobalConfig.JwtKey) tokenString, err := token.SignedString(jwtKeyBytes)
return tokenString, err return tokenString, err
} }

View File

@ -1,7 +1,22 @@
// Copyright 2024 Florian Beisel
//
// 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 auth package auth
import ( import (
"database/sql" "database/sql"
"log"
"net/http" "net/http"
"git.beisel.it/florian/hostname-service/db" "git.beisel.it/florian/hostname-service/db"
@ -11,19 +26,19 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// LoginHandler godoc // LoginHandler godoc
// //
// @Summary User login // @Summary User login
// @Description Authenticate user and return JWT token // @Description Authenticate user and return JWT token
// @Tags Authentication // @Tags Authentication
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param loginCredentials body models.LoginCredentials true "Login Credentials" // @Param loginCredentials body models.LoginCredentials true "Login Credentials"
// @Success 200 {object} models.TokenResponse "Successfully authenticated, JWT token returned" // @Success 200 {object} models.TokenResponse "Successfully authenticated, JWT token returned"
// @Failure 400 {object} models.ErrorResponse "Invalid request body" // @Failure 400 {object} models.ErrorResponse "Invalid request body"
// @Failure 401 {object} models.ErrorResponse "Invalid login credentials" // @Failure 401 {object} models.ErrorResponse "Invalid login credentials"
// @Failure 500 {object} models.ErrorResponse "Internal server error" // @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /login [post] // @Router /login [post]
func LoginHandler(c *gin.Context) { func LoginHandler(c *gin.Context) {
var creds models.LoginCredentials var creds models.LoginCredentials
@ -58,6 +73,7 @@ func LoginHandler(c *gin.Context) {
token, err := GenerateToken(storedCreds.Username) token, err := GenerateToken(storedCreds.Username)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
log.Printf("Error generating token %v", err)
return return
} }

View File

@ -1,10 +1,23 @@
// Copyright 2024 Florian Beisel
//
// 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 config package config
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"io/ioutil"
"os" "os"
"reflect" "reflect"
"strings" "strings"
@ -45,7 +58,7 @@ func LoadConfig() error {
loadConfigFromEnvOrSecrets(GlobalConfig) loadConfigFromEnvOrSecrets(GlobalConfig)
// Check if the configuration has been loaded successfully // Check if the configuration has been loaded successfully
if GlobalConfig.JwtKey == "" || GlobalConfig.DatabaseFile == "" { if GlobalConfig.DatabaseFile == "" {
// Add more checks as necessary for other required config fields // Add more checks as necessary for other required config fields
return errors.New("failed to load configuration from any source") return errors.New("failed to load configuration from any source")
} }
@ -54,7 +67,7 @@ func LoadConfig() error {
} }
func loadConfigFromFile(filePath string, config *AppConfig) error { func loadConfigFromFile(filePath string, config *AppConfig) error {
fileData, err := ioutil.ReadFile(filePath) fileData, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return err return err
} }
@ -81,7 +94,7 @@ func loadConfigFromEnvOrSecrets(config *AppConfig) {
// Handling Docker secrets (file-based secrets) // Handling Docker secrets (file-based secrets)
secretFileEnvVar := envVar + "_FILE" secretFileEnvVar := envVar + "_FILE"
if secretFilePath, exists := os.LookupEnv(secretFileEnvVar); exists { if secretFilePath, exists := os.LookupEnv(secretFileEnvVar); exists {
if secretValue, err := ioutil.ReadFile(secretFilePath); err == nil { if secretValue, err := os.ReadFile(secretFilePath); err == nil {
val.Field(i).SetString(string(secretValue)) val.Field(i).SetString(string(secretValue))
} }
} }

View File

@ -1,3 +1,17 @@
// Copyright 2024 Florian Beisel
//
// 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 db package db
import ( import (

View File

@ -15,8 +15,8 @@ const docTemplate = `{
"email": "florian@beisel.it" "email": "florian@beisel.it"
}, },
"license": { "license": {
"name": "MIT", "name": "Apache 2.0",
"url": "http://git.beisel.it/florian/hostname-service/" "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}, },
"version": "{{.Version}}" "version": "{{.Version}}"
}, },
@ -25,6 +25,11 @@ const docTemplate = `{
"paths": { "paths": {
"/api/notebook": { "/api/notebook": {
"put": { "put": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a new hostname for a notebook based on dynamic rules.", "description": "Generates a new hostname for a notebook based on dynamic rules.",
"consumes": [ "consumes": [
"application/json" "application/json"
@ -33,7 +38,7 @@ const docTemplate = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Generating Hostnames" "Manipulate existing Hostnames"
], ],
"summary": "Update hostname for category \"notebook\"", "summary": "Update hostname for category \"notebook\"",
"operationId": "update-notebook-hostname", "operationId": "update-notebook-hostname",
@ -58,6 +63,11 @@ const docTemplate = `{
} }
}, },
"post": { "post": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a hostname for a notebook based on dynamic rules.", "description": "Generates a hostname for a notebook based on dynamic rules.",
"consumes": [ "consumes": [
"application/json" "application/json"
@ -91,6 +101,110 @@ const docTemplate = `{
} }
} }
}, },
"/api/rules": {
"get": {
"description": "Get a list of all available hostname generation rules.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Rules"
],
"summary": "List Available Rules",
"responses": {
"200": {
"description": "List of available rules with descriptions",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/server": {
"put": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a new hostname for a notebook based on dynamic rules.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Manipulate existing Hostnames"
],
"summary": "Update hostname for category \"notebook\"",
"operationId": "update-server-hostname",
"parameters": [
{
"description": "Input data to generate hostname",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/rules.ServerRuleInput"
}
}
],
"responses": {
"200": {
"description": "Hostname",
"schema": {
"$ref": "#/definitions/models.SimpleHostnameResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a hostname for a notebook based on dynamic rules.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Generating Hostnames"
],
"summary": "Generate hostname for category \"notebook\"",
"operationId": "insert-server-hostname",
"parameters": [
{
"description": "Input data to generate hostname",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/rules.ServerRuleInput"
}
}
],
"responses": {
"200": {
"description": "Hostname",
"schema": {
"$ref": "#/definitions/models.SimpleHostnameResponse"
}
}
}
}
},
"/hello": { "/hello": {
"get": { "get": {
"security": [ "security": [
@ -374,6 +488,29 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
} }
},
"rules.ServerRuleInput": {
"type": "object",
"properties": {
"Description": {
"type": "string"
},
"ILO": {
"type": "string"
},
"IP": {
"type": "string"
},
"Location": {
"type": "string"
},
"OrgUnit": {
"type": "string"
},
"Responsible": {
"type": "string"
}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@ -9,8 +9,8 @@
"email": "florian@beisel.it" "email": "florian@beisel.it"
}, },
"license": { "license": {
"name": "MIT", "name": "Apache 2.0",
"url": "http://git.beisel.it/florian/hostname-service/" "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}, },
"version": "1" "version": "1"
}, },
@ -19,6 +19,11 @@
"paths": { "paths": {
"/api/notebook": { "/api/notebook": {
"put": { "put": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a new hostname for a notebook based on dynamic rules.", "description": "Generates a new hostname for a notebook based on dynamic rules.",
"consumes": [ "consumes": [
"application/json" "application/json"
@ -27,7 +32,7 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Generating Hostnames" "Manipulate existing Hostnames"
], ],
"summary": "Update hostname for category \"notebook\"", "summary": "Update hostname for category \"notebook\"",
"operationId": "update-notebook-hostname", "operationId": "update-notebook-hostname",
@ -52,6 +57,11 @@
} }
}, },
"post": { "post": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a hostname for a notebook based on dynamic rules.", "description": "Generates a hostname for a notebook based on dynamic rules.",
"consumes": [ "consumes": [
"application/json" "application/json"
@ -85,6 +95,110 @@
} }
} }
}, },
"/api/rules": {
"get": {
"description": "Get a list of all available hostname generation rules.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Rules"
],
"summary": "List Available Rules",
"responses": {
"200": {
"description": "List of available rules with descriptions",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/server": {
"put": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a new hostname for a notebook based on dynamic rules.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Manipulate existing Hostnames"
],
"summary": "Update hostname for category \"notebook\"",
"operationId": "update-server-hostname",
"parameters": [
{
"description": "Input data to generate hostname",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/rules.ServerRuleInput"
}
}
],
"responses": {
"200": {
"description": "Hostname",
"schema": {
"$ref": "#/definitions/models.SimpleHostnameResponse"
}
}
}
},
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a hostname for a notebook based on dynamic rules.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Generating Hostnames"
],
"summary": "Generate hostname for category \"notebook\"",
"operationId": "insert-server-hostname",
"parameters": [
{
"description": "Input data to generate hostname",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/rules.ServerRuleInput"
}
}
],
"responses": {
"200": {
"description": "Hostname",
"schema": {
"$ref": "#/definitions/models.SimpleHostnameResponse"
}
}
}
}
},
"/hello": { "/hello": {
"get": { "get": {
"security": [ "security": [
@ -368,6 +482,29 @@
"type": "string" "type": "string"
} }
} }
},
"rules.ServerRuleInput": {
"type": "object",
"properties": {
"Description": {
"type": "string"
},
"ILO": {
"type": "string"
},
"IP": {
"type": "string"
},
"Location": {
"type": "string"
},
"OrgUnit": {
"type": "string"
},
"Responsible": {
"type": "string"
}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@ -58,6 +58,21 @@ definitions:
OrgUnit: OrgUnit:
type: string type: string
type: object type: object
rules.ServerRuleInput:
properties:
Description:
type: string
ILO:
type: string
IP:
type: string
Location:
type: string
OrgUnit:
type: string
Responsible:
type: string
type: object
host: localhost:8080 host: localhost:8080
info: info:
contact: contact:
@ -66,8 +81,8 @@ info:
url: http://git.beisel.it/florian url: http://git.beisel.it/florian
description: This is a sample server for a hostname service. description: This is a sample server for a hostname service.
license: license:
name: MIT name: Apache 2.0
url: http://git.beisel.it/florian/hostname-service/ url: http://www.apache.org/licenses/LICENSE-2.0.html
title: Hostname Service API title: Hostname Service API
version: "1" version: "1"
paths: paths:
@ -168,6 +183,8 @@ paths:
description: Hostname description: Hostname
schema: schema:
$ref: '#/definitions/models.SimpleHostnameResponse' $ref: '#/definitions/models.SimpleHostnameResponse'
security:
- Bearer: []
summary: Generate hostname for category "notebook" summary: Generate hostname for category "notebook"
tags: tags:
- Generating Hostnames - Generating Hostnames
@ -190,9 +207,77 @@ paths:
description: Hostname description: Hostname
schema: schema:
$ref: '#/definitions/models.SimpleHostnameResponse' $ref: '#/definitions/models.SimpleHostnameResponse'
security:
- Bearer: []
summary: Update hostname for category "notebook" summary: Update hostname for category "notebook"
tags: tags:
- Manipulate existing Hostnames
/api/rules:
get:
consumes:
- application/json
description: Get a list of all available hostname generation rules.
produces:
- application/json
responses:
"200":
description: List of available rules with descriptions
schema:
additionalProperties:
type: string
type: object
summary: List Available Rules
tags:
- Rules
/api/server:
post:
consumes:
- application/json
description: Generates a hostname for a notebook based on dynamic rules.
operationId: insert-server-hostname
parameters:
- description: Input data to generate hostname
in: body
name: body
required: true
schema:
$ref: '#/definitions/rules.ServerRuleInput'
produces:
- application/json
responses:
"200":
description: Hostname
schema:
$ref: '#/definitions/models.SimpleHostnameResponse'
security:
- Bearer: []
summary: Generate hostname for category "notebook"
tags:
- Generating Hostnames - Generating Hostnames
put:
consumes:
- application/json
description: Generates a new hostname for a notebook based on dynamic rules.
operationId: update-server-hostname
parameters:
- description: Input data to generate hostname
in: body
name: body
required: true
schema:
$ref: '#/definitions/rules.ServerRuleInput'
produces:
- application/json
responses:
"200":
description: Hostname
schema:
$ref: '#/definitions/models.SimpleHostnameResponse'
security:
- Bearer: []
summary: Update hostname for category "notebook"
tags:
- Manipulate existing Hostnames
/hello: /hello:
get: get:
consumes: consumes:

View File

@ -1,3 +1,17 @@
# Copyright 2024 Florian Beisel
#
# 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.
# Define API URLs # Define API URLs
$loginUrl = "http://localhost:8080/api/v1/login" $loginUrl = "http://localhost:8080/api/v1/login"
$generateNotebookUrl = "http://localhost:8080/api/v1/notebook" $generateNotebookUrl = "http://localhost:8080/api/v1/notebook"

51
main.go
View File

@ -19,15 +19,9 @@ package main
import ( import (
"log" "log"
"git.beisel.it/florian/hostname-service/api"
"git.beisel.it/florian/hostname-service/auth"
"git.beisel.it/florian/hostname-service/config" "git.beisel.it/florian/hostname-service/config"
"git.beisel.it/florian/hostname-service/db" "git.beisel.it/florian/hostname-service/db"
"git.beisel.it/florian/hostname-service/docs" "git.beisel.it/florian/hostname-service/router"
"git.beisel.it/florian/hostname-service/middleware"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
) )
// @title Hostname Service API // @title Hostname Service API
@ -54,48 +48,9 @@ func main() {
log.Fatalf("Failed to load configuration: %v", err) log.Fatalf("Failed to load configuration: %v", err)
} }
gin.SetMode(gin.DebugMode)
db.Init(config.GlobalConfig.DatabaseFile) db.Init(config.GlobalConfig.DatabaseFile)
router := gin.Default() router := router.New()
docs.SwaggerInfo.Host = "localhost:8080"
docs.SwaggerInfo.BasePath = "/api/v1"
v1 := router.Group("/api/v1")
{
// public routes
v1.POST("/login", auth.LoginHandler)
// Protected Routes
authenticated := v1.Group("/").Use(middleware.Authenticate())
{
authenticated.GET("/hello", api.Helloworld)
// Create Host
authenticated.POST("/:category", func(c *gin.Context) {
api.CreateOrUpdateHostname(c, false)
})
// Get Host Details
authenticated.GET("/:category/:hostname", api.GetHostnameByCategoryAndName)
// Update Host
authenticated.PUT("/:category/:oldhostname", func(c *gin.Context) {
api.CreateOrUpdateHostname(c, true)
})
// Delete Host
authenticated.DELETE("/:category/:hostname", api.DeleteHostname)
// List Hostnames
authenticated.GET("/:category", api.ListHostnamesByCategory)
}
}
// Swagger endpoint
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
router.Run(":8080") router.Run(":8080")
} }

View File

@ -23,7 +23,8 @@ func Authenticate() gin.HandlerFunc {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method") return nil, fmt.Errorf("unexpected signing method")
} }
return config.GlobalConfig.JwtKey, nil jwtKeyBytes := []byte(config.GlobalConfig.JwtKey)
return jwtKeyBytes, nil
}) })
if err != nil { if err != nil {

View File

@ -1,3 +1,17 @@
// Copyright 2024 Florian Beisel
//
// 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 models package models
type ErrorResponse struct { type ErrorResponse struct {

View File

@ -1,3 +1,17 @@
// Copyright 2024 Florian Beisel
//
// 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 models package models
import "time" import "time"

View File

@ -1,3 +1,17 @@
// Copyright 2024 Florian Beisel
//
// 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 models package models
// User Credentials Model // User Credentials Model

View File

@ -1,3 +1,17 @@
// Copyright 2024 Florian Beisel
//
// 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 models package models
// User Model // User Model

73
router/router.go Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2024 Florian Beisel
//
// 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 router
import (
"git.beisel.it/florian/hostname-service/api"
"git.beisel.it/florian/hostname-service/auth"
"git.beisel.it/florian/hostname-service/docs"
"git.beisel.it/florian/hostname-service/middleware"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
// ... other necessary imports ...
)
func New() *gin.Engine {
gin.SetMode(gin.DebugMode)
router := gin.Default()
docs.SwaggerInfo.Host = "localhost:8080"
docs.SwaggerInfo.BasePath = "/api/v1"
v1 := router.Group("/api/v1")
{
// public routes
v1.POST("/login", auth.LoginHandler)
// Protected Routes
authenticated := v1.Group("/").Use(middleware.Authenticate())
{
authenticated.GET("/hello", api.Helloworld)
// Create Host
authenticated.POST("/:category", func(c *gin.Context) {
api.CreateOrUpdateHostname(c, false)
})
// Get Host Details
authenticated.GET("/:category/:hostname", api.GetHostnameByCategoryAndName)
// Update Host
authenticated.PUT("/:category/:oldhostname", func(c *gin.Context) {
api.CreateOrUpdateHostname(c, true)
})
// Delete Host
authenticated.DELETE("/:category/:hostname", api.DeleteHostname)
// List Hostnames
authenticated.GET("/:category", api.ListHostnamesByCategory)
// List available Rules
authenticated.GET("/api/rules", api.ListAvailableRules)
}
}
// Swagger endpoint
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return router
}

View File

@ -9,41 +9,53 @@ import (
type HostnameRule interface { type HostnameRule interface {
Generate(params map[string]interface{}) (string, []byte, error) Generate(params map[string]interface{}) (string, []byte, error)
Insert(category string, hostname string, paramsJSON []byte) error Insert(category string, params map[string]interface{}) (string, error)
Update(category string, oldhostname string, hostname string, paramsJSON []byte) error Update(category string, oldhostname string, params map[string]interface{}) (string, error)
} }
type BaseRule struct{} type BaseRule struct{}
func (br *BaseRule) baseInsert(category string, hostname string, paramsJSON []byte) error { func (br *BaseRule) baseInsert(rule HostnameRule, category string, params map[string]interface{}) (string, error) {
// Generate the hostname using the passed rule's Generate method
hostname, paramsJSON, err := rule.Generate(params)
if err != nil {
return "", err
}
exists, err := db.HostnameExists(category, hostname) exists, err := db.HostnameExists(category, hostname)
if err != nil { if err != nil {
return fmt.Errorf("error checking existence of hostname: %v", err.Error()) log.Printf("error checking existence of hostname: %v", err)
return "", err
} }
if exists { if exists {
return fmt.Errorf("hostname %s does not exist in category %s", hostname, category) log.Printf("hostname %s does not exist in category %s", hostname, category)
return "", fmt.Errorf("hostname-exists")
} }
// Insert the hostname into the database
err = db.InsertHostname(category, hostname, paramsJSON) err = db.InsertHostname(category, hostname, paramsJSON)
if err != nil { if err != nil {
log.Printf("Error inserting hostname into DB: %v", err) log.Printf("Error inserting hostname into DB: %v", err)
return err return "", err
}
return nil
}
func (br *BaseRule) baseUpdate(category string, oldhostname string, hostname string, paramsJSON []byte) error {
exists, err := db.HostnameExists(category, oldhostname)
if err != nil {
return fmt.Errorf("error checking existence of hostname: %v", err.Error())
}
if !exists {
return fmt.Errorf("hostname %s does not exist in category %s", oldhostname, category)
} }
err = db.UpdateHostname(category, oldhostname, hostname, paramsJSON) return hostname, nil
if err != nil { }
log.Printf("Error inserting hostname into DB: %v", err)
return err // baseUpdate method for BaseRule
} func (br *BaseRule) baseUpdate(rule HostnameRule, category string, oldhostname string, params map[string]interface{}) (string, error) {
return nil // Generate the new hostname using the passed rule's Generate method
newHostname, paramsJSON, err := rule.Generate(params)
if err != nil {
return "", err
}
// Update the hostname in the database
err = db.UpdateHostname(category, oldhostname, newHostname, paramsJSON)
if err != nil {
log.Printf("Error updating hostname in DB: %v", err)
return "", err
}
return newHostname, nil
} }

View File

@ -75,8 +75,11 @@ func (nr *NotebookRule) Generate(params map[string]interface{}) (string, []byte,
// @Param body body NotebookRuleInput true "Input data to generate hostname" // @Param body body NotebookRuleInput true "Input data to generate hostname"
// @Success 200 {object} models.SimpleHostnameResponse "Hostname" // @Success 200 {object} models.SimpleHostnameResponse "Hostname"
// @Router /api/notebook [post] // @Router /api/notebook [post]
func (nr *NotebookRule) Insert(category string, hostname string, paramsJSON []byte) error { // @Security Bearer
return nr.baseInsert(category, hostname, paramsJSON) func (nr *NotebookRule) Insert(category string, params map[string]interface{}) (string, error) {
// Generate the hostname
return nr.baseInsert(nr, category, params)
} }
// @Summary Update hostname for category "notebook" // @Summary Update hostname for category "notebook"
@ -84,10 +87,11 @@ func (nr *NotebookRule) Insert(category string, hostname string, paramsJSON []by
// @ID update-notebook-hostname // @ID update-notebook-hostname
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Tags Generating Hostnames // @Tags Manipulate existing Hostnames
// @Param body body NotebookRuleInput true "Input data to generate hostname" // @Param body body NotebookRuleInput true "Input data to generate hostname"
// @Success 200 {object} models.SimpleHostnameResponse "Hostname" // @Success 200 {object} models.SimpleHostnameResponse "Hostname"
// @Router /api/notebook [put] // @Router /api/notebook [put]
func (nr *NotebookRule) Update(category string, oldhostname string, hostname string, paramsJSON []byte) error { // @Security Bearer
return nr.baseUpdate(category, oldhostname, hostname, paramsJSON) func (nr *NotebookRule) Update(category string, oldhostname string, params map[string]interface{}) (string, error) {
return nr.baseUpdate(nr, category, oldhostname, params)
} }

34
rules/registry.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2024 Florian Beisel
//
// 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 rules
type RuleFactoryFunc func() HostnameRule
type RuleDescriptor struct {
Description string
Factory RuleFactoryFunc
}
var RulesRegistry = map[string]RuleDescriptor{
"notebook": {
Description: "Generates hostnames for notebooks.",
Factory: func() HostnameRule { return &NotebookRule{} },
},
"server": {
Description: "Generates hostnames for servers.",
Factory: func() HostnameRule { return &ServerRule{} },
},
// ... other rules ...
}

139
rules/server.go Normal file
View File

@ -0,0 +1,139 @@
// Copyright 2024 Florian Beisel
//
// 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 rules
import (
"encoding/json"
"errors"
"fmt"
"log"
"git.beisel.it/florian/hostname-service/db"
)
type ServerRule struct {
BaseRule
OrgUnit string `json:"OrgUnit"`
Location string `json:"Location"`
Number int `json:"Number"`
Description string `json:"Description"`
IP string `json:"IP"`
ILO string `json:"ILO"`
Responsible string `json:"Responsible"`
}
type ServerRuleInput struct {
OrgUnit string `json:"OrgUnit"`
Location string `json:"Location"`
Description string `json:"Description"`
IP string `json:"IP"`
ILO string `json:"ILO"`
Responsible string `json:"Responsible"`
}
// Ensure that ServerRule implements HostnameRule interface
var _ HostnameRule = &ServerRule{}
func (sr *ServerRule) Generate(params map[string]interface{}) (string, []byte, error) {
var ok bool
// Get the OrgUnit
if sr.OrgUnit, ok = params["OrgUnit"].(string); !ok {
return "", nil, errors.New("OrgUnit parameter is required and must be a string")
}
// Get the Location
if sr.Location, ok = params["Location"].(string); !ok {
return "", nil, errors.New("location parameter is required and must be a string")
}
// Get the Description
if sr.Description, ok = params["Description"].(string); !ok {
return "", nil, errors.New("parameter Description is required and must be a string")
}
// Get the IP
if sr.IP, ok = params["IP"].(string); !ok {
return "", nil, errors.New("IP parameter is required and must be a string")
}
// Get the ILO IP
if sr.ILO, ok = params["ILO"].(string); !ok {
return "", nil, errors.New("ILO parameter is required and must be a string")
}
// Get the Responsible
if sr.Responsible, ok = params["Responsible"].(string); !ok {
return "", nil, errors.New("parameter Responsible parameter is required and must be a string")
}
// Get last used number from the database and increment it
maxNumber, err := db.GetMaxNumberForCategory("server")
if err != nil {
return "", nil, err
}
newNumber := 1
if maxNumber >= 1 {
newNumber = int(maxNumber) + 1
}
sr.Number = newNumber
// Generate the hostname (orgUnit + Location + "NB" + Number)
hostname := fmt.Sprintf("%s%sSV%04d", sr.OrgUnit, sr.Location, sr.Number)
// Store the generated hostname in the database
// JSON parameters can be stored by marshalling the params map
paramsJSON, err := json.Marshal(sr)
if err != nil {
log.Printf("Error converting Category Struct to JSON: %v", err)
return "", nil, err
}
// Return the generated hostname and marshaled parameters
return hostname, paramsJSON, nil
}
// @Summary Generate hostname for category "notebook"
// @Description Generates a hostname for a notebook based on dynamic rules.
// @ID insert-server-hostname
// @Accept json
// @Produce json
// @Tags Generating Hostnames
// @Param body body ServerRuleInput true "Input data to generate hostname"
// @Success 200 {object} models.SimpleHostnameResponse "Hostname"
// @Router /api/server [post]
// @Security Bearer
func (nr *ServerRule) Insert(category string, params map[string]interface{}) (string, error) {
// Generate the hostname
return nr.baseInsert(nr, category, params)
}
// @Summary Update hostname for category "notebook"
// @Description Generates a new hostname for a notebook based on dynamic rules.
// @ID update-server-hostname
// @Accept json
// @Produce json
// @Tags Manipulate existing Hostnames
// @Param body body ServerRuleInput true "Input data to generate hostname"
// @Success 200 {object} models.SimpleHostnameResponse "Hostname"
// @Router /api/server [put]
// @Security Bearer
func (nr *ServerRule) Update(category string, oldhostname string, params map[string]interface{}) (string, error) {
return nr.baseUpdate(nr, category, oldhostname, params)
}