Skip to content

Commit

Permalink
Merge pull request #353 from kotalco/352-create-tls-store-for-letsenc…
Browse files Browse the repository at this point in the history
…rypt-or-custom-certificate

352 create tls store for letsencrypt or custom certificate
  • Loading branch information
mFarghaly authored May 2, 2024
2 parents 6cd2f2f + f771b6c commit 8de50f5
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 3 deletions.
16 changes: 15 additions & 1 deletion api/handler/setting/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,19 @@ var (
)

func ConfigureDomain(c *fiber.Ctx) error {
userId := c.Locals("user").(token.UserDetails).ID
userDetails, restErr := userService.WithoutTransaction().GetById(userId)
if restErr != nil {
return c.Status(restErr.StatusCode()).JSON(restErr)
}

dto := new(setting.ConfigureDomainRequestDto)
if err := c.BodyParser(dto); err != nil {
badReq := restErrors.NewBadRequestError("invalid request body")
return c.Status(badReq.StatusCode()).JSON(badReq)
}

restErr := setting.Validate(dto)
restErr = setting.Validate(dto)
if restErr != nil {
return c.Status(restErr.StatusCode()).JSON(restErr)
}
Expand Down Expand Up @@ -99,6 +105,14 @@ func ConfigureDomain(c *fiber.Ctx) error {
return c.Status(err.StatusCode()).JSON(err)
}

//configure lets encrypt
restErr = tlsCertificateService.ConfigureLetsEncrypt(setting.KotalLetsEncryptResolverName, userDetails.Email)
if restErr != nil {
logger.Error("CONFIGURE_TLS", restErr)
sqlclient.Rollback(txHandle)
return c.Status(restErr.StatusCode()).JSON(restErr)
}

sqlclient.Commit(txHandle)
return c.Status(http.StatusOK).JSON(responder.NewResponse(responder.SuccessMessage{Message: "domain configured successfully!"}))
}
Expand Down
144 changes: 144 additions & 0 deletions api/handler/setting/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/kotalco/core-api/core/setting"
"github.com/kotalco/core-api/core/user"
"github.com/kotalco/core-api/k8s/ingressroute"
"github.com/kotalco/core-api/k8s/middleware"
restErrors "github.com/kotalco/core-api/pkg/errors"
Expand All @@ -15,6 +16,7 @@ import (
traefikv1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"gorm.io/gorm"
"io/ioutil"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
Expand Down Expand Up @@ -132,6 +134,110 @@ func (k k8MiddlewareServiceMock) Create(dto *middleware.CreateMiddlewareDto) res
return k8middlewareCreateFunc(dto)
}

type tlsCertificateServiceMock struct{}

var (
tlsGetTraefikDeploymentFunc func() (*appsv1.Deployment, restErrors.IRestErr)
tlsConfigureLetsEncryptFunc func(resolverNme string, acmeEmail string) restErrors.IRestErr
tlsConfigureCustomCertificateFunc func(secretName string) restErrors.IRestErr
)

func (tls tlsCertificateServiceMock) GetTraefikDeployment() (*appsv1.Deployment, restErrors.IRestErr) {
return tlsGetTraefikDeploymentFunc()
}
func (tls tlsCertificateServiceMock) ConfigureLetsEncrypt(resolverNme string, acmeEmail string) restErrors.IRestErr {
return tlsConfigureLetsEncryptFunc(resolverNme, acmeEmail)
}
func (tls tlsCertificateServiceMock) ConfigureCustomCertificate(secretName string) restErrors.IRestErr {
return tlsConfigureCustomCertificateFunc(secretName)
}

var (
UserWithTransactionFunc func(txHandle *gorm.DB) user.IService
SignUpFunc func(dto *user.SignUpRequestDto) (*user.User, restErrors.IRestErr)
SignInFunc func(dto *user.SignInRequestDto) (*user.UserSessionResponseDto, restErrors.IRestErr)
VerifyTOTPFunc func(model *user.User, totp string) (*user.UserSessionResponseDto, restErrors.IRestErr)
GetByEmailFunc func(email string) (*user.User, restErrors.IRestErr)
GetByIdFunc func(Id string) (*user.User, restErrors.IRestErr)
VerifyEmailFunc func(model *user.User) restErrors.IRestErr
ResetPasswordFunc func(model *user.User, password string) restErrors.IRestErr
ChangePasswordFunc func(model *user.User, dto *user.ChangePasswordRequestDto) restErrors.IRestErr
ChangeEmailFunc func(model *user.User, dto *user.ChangeEmailRequestDto) restErrors.IRestErr
CreateTOTPFunc func(model *user.User, dto *user.CreateTOTPRequestDto) (bytes.Buffer, restErrors.IRestErr)
EnableTwoFactorAuthFunc func(model *user.User, totp string) (*user.User, restErrors.IRestErr)
DisableTwoFactorAuthFunc func(model *user.User, dto *user.DisableTOTPRequestDto) restErrors.IRestErr
FindWhereIdInSliceFunc func(ids []string) ([]*user.User, restErrors.IRestErr)
usersCountFunc func() (int64, restErrors.IRestErr)
usersSetAsPlatformAdminFunc func(model *user.User) restErrors.IRestErr
)

type userServiceMock struct{}

func (uService userServiceMock) WithoutTransaction() user.IService {
return uService
}

func (uService userServiceMock) WithTransaction(txHandle *gorm.DB) user.IService {
return uService
}

func (userServiceMock) SignUp(dto *user.SignUpRequestDto) (*user.User, restErrors.IRestErr) {
return SignUpFunc(dto)
}

func (userServiceMock) SignIn(dto *user.SignInRequestDto) (*user.UserSessionResponseDto, restErrors.IRestErr) {
return SignInFunc(dto)
}

func (userServiceMock) GetByEmail(email string) (*user.User, restErrors.IRestErr) {
return GetByEmailFunc(email)
}

func (userServiceMock) GetById(Id string) (*user.User, restErrors.IRestErr) {
return GetByIdFunc(Id)
}

func (userServiceMock) VerifyEmail(model *user.User) restErrors.IRestErr {
return VerifyEmailFunc(model)
}

func (userServiceMock) ResetPassword(model *user.User, password string) restErrors.IRestErr {
return ResetPasswordFunc(model, password)
}

func (userServiceMock) ChangePassword(model *user.User, dto *user.ChangePasswordRequestDto) restErrors.IRestErr {
return ChangePasswordFunc(model, dto)
}

func (userServiceMock) ChangeEmail(model *user.User, dto *user.ChangeEmailRequestDto) restErrors.IRestErr {
return ChangeEmailFunc(model, dto)
}

func (userServiceMock) CreateTOTP(model *user.User, dto *user.CreateTOTPRequestDto) (bytes.Buffer, restErrors.IRestErr) {
return CreateTOTPFunc(model, dto)
}

func (userServiceMock) EnableTwoFactorAuth(model *user.User, totp string) (*user.User, restErrors.IRestErr) {
return EnableTwoFactorAuthFunc(model, totp)
}

func (userServiceMock) VerifyTOTP(model *user.User, totp string) (*user.UserSessionResponseDto, restErrors.IRestErr) {
return VerifyTOTPFunc(model, totp)
}

func (userServiceMock) DisableTwoFactorAuth(model *user.User, dto *user.DisableTOTPRequestDto) restErrors.IRestErr {
return DisableTwoFactorAuthFunc(model, dto)
}
func (userServiceMock) FindWhereIdInSlice(ids []string) ([]*user.User, restErrors.IRestErr) {
return FindWhereIdInSliceFunc(ids)
}
func (userServiceMock) Count() (int64, restErrors.IRestErr) {
return usersCountFunc()
}
func (userServiceMock) SetAsPlatformAdmin(model *user.User) restErrors.IRestErr {
return usersSetAsPlatformAdminFunc(model)
}

func newFiberCtx(dto interface{}, method func(c *fiber.Ctx) error, locals map[string]interface{}) ([]byte, *http.Response) {
app := fiber.New()
app.Post("/test/", func(c *fiber.Ctx) error {
Expand Down Expand Up @@ -165,6 +271,8 @@ func TestMain(m *testing.M) {
settingService = &settingServiceMocks{}
k8service = &k8sServiceMock{}
ingressRouteService = &ingressRouteServiceMock{}
userService = &userServiceMock{}
tlsCertificateService = &tlsCertificateServiceMock{}

code := m.Run()
os.Exit(code)
Expand All @@ -186,6 +294,12 @@ func TestConfigureDomain(t *testing.T) {
var invalidDto = map[string]string{}

t.Run("ConfigureDomain should pass with ip address", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
tlsConfigureLetsEncryptFunc = func(resolverNme string, acmeEmail string) restErrors.IRestErr {
return nil
}
networkIdentifiers = func() (ip string, hostName string, restErr restErrors.IRestErr) {
return "1223", "", nil
}
Expand Down Expand Up @@ -226,6 +340,12 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "domain configured successfully!", result["data"].Message)
})
t.Run("ConfigureDomain should pass with hostname", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
tlsConfigureLetsEncryptFunc = func(resolverNme string, acmeEmail string) restErrors.IRestErr {
return nil
}
networkIdentifiers = func() (ip string, hostName string, restErr restErrors.IRestErr) {
return "", "hostname.amazon.com", nil
}
Expand Down Expand Up @@ -266,6 +386,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "domain configured successfully!", result["data"].Message)
})
t.Run("configure domain should throw bad request err", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
body, resp := newFiberCtx("", ConfigureDomain, locals)
var result restErrors.RestErr
err := json.Unmarshal(body, &result)
Expand All @@ -274,6 +397,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "invalid request body", result.Message)
})
t.Run("configure domain should throw validation err", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
body, resp := newFiberCtx(invalidDto, ConfigureDomain, locals)
var result restErrors.RestErr
err := json.Unmarshal(body, &result)
Expand All @@ -287,6 +413,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("ConfigureDomain should throw if can't get ip address or hostname", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
networkIdentifiers = func() (ip string, hostname string, restErr restErrors.IRestErr) {
return "", "", restErrors.NewInternalServerError("can't get ip address")
}
Expand All @@ -302,6 +431,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "can't get ip address", result.Message)
})
t.Run("ConfigureDomain should throw if can't verify domain ip", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
networkIdentifiers = func() (ip string, hostName string, restErr restErrors.IRestErr) {
return "1.2.3", "", nil
}
Expand All @@ -320,6 +452,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "can't verify domain", result.Message)
})
t.Run("ConfigureDomain should throw if can't verify domain hostname", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
networkIdentifiers = func() (ip string, hostName string, restErr restErrors.IRestErr) {
return "", "111.amazon.com", nil
}
Expand All @@ -338,6 +473,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "can't verify domain", result.Message)
})
t.Run("configure domain should throw if service throws", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
networkIdentifiers = func() (ip string, hostName string, restErr restErrors.IRestErr) {
return "1223", "", nil
}
Expand All @@ -356,6 +494,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, http.StatusInternalServerError, resp.StatusCode)
})
t.Run("ConfigureDomain should throw if can't get kotal stack ingress-route", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
networkIdentifiers = func() (ip string, hostname string, restErr restErrors.IRestErr) {
return "1223", "", nil
}
Expand All @@ -381,6 +522,9 @@ func TestConfigureDomain(t *testing.T) {
assert.EqualValues(t, "no such record", result.Message)
})
t.Run("ConfigureDomain should throw if can't update kotal stack", func(t *testing.T) {
GetByIdFunc = func(Id string) (*user.User, restErrors.IRestErr) {
return &user.User{Email: "email.com"}, nil
}
networkIdentifiers = func() (ip string, hostName string, restErr restErrors.IRestErr) {
return "1223", "", nil
}
Expand Down
36 changes: 34 additions & 2 deletions k8s/tlscertificate/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"fmt"
"github.com/kotalco/core-api/config"
"github.com/kotalco/core-api/core/setting"
"github.com/kotalco/core-api/k8s"
restErrors "github.com/kotalco/core-api/pkg/errors"
"github.com/kotalco/core-api/pkg/logger"
traefikv1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/tls"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -38,6 +40,27 @@ func (t *tlsCertificate) GetTraefikDeployment() (*appsv1.Deployment, restErrors.
}

func (t *tlsCertificate) ConfigureLetsEncrypt(resolverNme string, acmeEmail string) restErrors.IRestErr {
//delete default tls-store if exists
tlsStore := &traefikv1alpha1.TLSStore{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: config.Environment.TraefikNamespace,
},
}
_ = k8sClient.Delete(context.Background(), tlsStore)

//create tls store
tlsStore = &traefikv1alpha1.TLSStore{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: config.Environment.TraefikNamespace,
},
Spec: traefikv1alpha1.TLSStoreSpec{
DefaultGeneratedCert: &tls.GeneratedCert{Resolver: setting.KotalLetsEncryptResolverName},
},
}
_ = k8sClient.Create(context.Background(), tlsStore)

deploy, restErr := t.GetTraefikDeployment()
if restErr != nil {
return restErr
Expand Down Expand Up @@ -75,8 +98,17 @@ func (t *tlsCertificate) ConfigureLetsEncrypt(resolverNme string, acmeEmail stri
}

func (t *tlsCertificate) ConfigureCustomCertificate(secretName string) restErrors.IRestErr {
//delete default tls-store if exists
tlsStore := &traefikv1alpha1.TLSStore{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: config.Environment.TraefikNamespace,
},
}
_ = k8sClient.Delete(context.Background(), tlsStore)

//create tls store
record := &traefikv1alpha1.TLSStore{
tlsStore = &traefikv1alpha1.TLSStore{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: config.Environment.TraefikNamespace,
Expand All @@ -85,7 +117,7 @@ func (t *tlsCertificate) ConfigureCustomCertificate(secretName string) restError
DefaultCertificate: &traefikv1alpha1.Certificate{SecretName: secretName},
},
}
_ = k8sClient.Create(context.Background(), record)
_ = k8sClient.Create(context.Background(), tlsStore)

//remove letsEncrypt static configuration
deploy, restErr := t.GetTraefikDeployment()
Expand Down

0 comments on commit 8de50f5

Please sign in to comment.