Compare commits

..

No commits in common. "2b41bbdbb9f69ff950a50b01e2da73a98b808616" and "8d2f4110735967ad48df034dcf19c66f8209f723" have entirely different histories.

23 changed files with 258 additions and 1014 deletions

View File

@ -1,37 +0,0 @@
// 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")
}

161
api/handlers.go Normal file
View File

@ -0,0 +1,161 @@
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")
}

View File

@ -1,137 +0,0 @@
// 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)
}

View File

@ -1,38 +0,0 @@
// 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,17 +1,3 @@
// 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 (
@ -22,15 +8,14 @@ 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(jwtKeyBytes) tokenString, err := token.SignedString(config.GlobalConfig.JwtKey)
return tokenString, err return tokenString, err
} }

View File

@ -1,22 +1,7 @@
// 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"
@ -73,7 +58,6 @@ 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,23 +1,10 @@
// 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"
@ -58,7 +45,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.DatabaseFile == "" { if GlobalConfig.JwtKey == "" || 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")
} }
@ -67,7 +54,7 @@ func LoadConfig() error {
} }
func loadConfigFromFile(filePath string, config *AppConfig) error { func loadConfigFromFile(filePath string, config *AppConfig) error {
fileData, err := os.ReadFile(filePath) fileData, err := ioutil.ReadFile(filePath)
if err != nil { if err != nil {
return err return err
} }
@ -94,7 +81,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 := os.ReadFile(secretFilePath); err == nil { if secretValue, err := ioutil.ReadFile(secretFilePath); err == nil {
val.Field(i).SetString(string(secretValue)) val.Field(i).SetString(string(secretValue))
} }
} }

View File

@ -1,17 +1,3 @@
// 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": "Apache 2.0", "name": "MIT",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html" "url": "http://git.beisel.it/florian/hostname-service/"
}, },
"version": "{{.Version}}" "version": "{{.Version}}"
}, },
@ -25,11 +25,6 @@ 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"
@ -38,7 +33,7 @@ const docTemplate = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Manipulate existing Hostnames" "Generating Hostnames"
], ],
"summary": "Update hostname for category \"notebook\"", "summary": "Update hostname for category \"notebook\"",
"operationId": "update-notebook-hostname", "operationId": "update-notebook-hostname",
@ -63,11 +58,6 @@ 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"
@ -101,110 +91,6 @@ 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": [
@ -488,29 +374,6 @@ 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": "Apache 2.0", "name": "MIT",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html" "url": "http://git.beisel.it/florian/hostname-service/"
}, },
"version": "1" "version": "1"
}, },
@ -19,11 +19,6 @@
"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"
@ -32,7 +27,7 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Manipulate existing Hostnames" "Generating Hostnames"
], ],
"summary": "Update hostname for category \"notebook\"", "summary": "Update hostname for category \"notebook\"",
"operationId": "update-notebook-hostname", "operationId": "update-notebook-hostname",
@ -57,11 +52,6 @@
} }
}, },
"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"
@ -95,110 +85,6 @@
} }
} }
}, },
"/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": [
@ -482,29 +368,6 @@
"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,21 +58,6 @@ 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:
@ -81,8 +66,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: Apache 2.0 name: MIT
url: http://www.apache.org/licenses/LICENSE-2.0.html url: http://git.beisel.it/florian/hostname-service/
title: Hostname Service API title: Hostname Service API
version: "1" version: "1"
paths: paths:
@ -183,8 +168,6 @@ 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
@ -207,77 +190,9 @@ 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,17 +1,3 @@
# 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,9 +19,15 @@ 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/router" "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"
) )
// @title Hostname Service API // @title Hostname Service API
@ -48,9 +54,48 @@ 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 := router.New() router := gin.Default()
router.Run(":8080")
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")
} }

View File

@ -23,8 +23,7 @@ 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")
} }
jwtKeyBytes := []byte(config.GlobalConfig.JwtKey) return config.GlobalConfig.JwtKey, nil
return jwtKeyBytes, nil
}) })
if err != nil { if err != nil {

View File

@ -1,17 +1,3 @@
// 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,17 +1,3 @@
// 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,17 +1,3 @@
// 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,17 +1,3 @@
// 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

View File

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

View File

@ -75,11 +75,8 @@ 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]
// @Security Bearer func (nr *NotebookRule) Insert(category string, hostname string, paramsJSON []byte) error {
func (nr *NotebookRule) Insert(category string, params map[string]interface{}) (string, error) { return nr.baseInsert(category, hostname, paramsJSON)
// Generate the hostname
return nr.baseInsert(nr, category, params)
} }
// @Summary Update hostname for category "notebook" // @Summary Update hostname for category "notebook"
@ -87,11 +84,10 @@ func (nr *NotebookRule) Insert(category string, params map[string]interface{}) (
// @ID update-notebook-hostname // @ID update-notebook-hostname
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Tags Manipulate existing Hostnames // @Tags Generating 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]
// @Security Bearer func (nr *NotebookRule) Update(category string, oldhostname string, hostname string, paramsJSON []byte) error {
func (nr *NotebookRule) Update(category string, oldhostname string, params map[string]interface{}) (string, error) { return nr.baseUpdate(category, oldhostname, hostname, paramsJSON)
return nr.baseUpdate(nr, category, oldhostname, params)
} }

View File

@ -1,34 +0,0 @@
// 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 ...
}

View File

@ -1,139 +0,0 @@
// 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)
}