domain.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. package domain
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "strings"
  7. "github.com/porter-dev/porter/internal/models"
  8. "github.com/porter-dev/porter/internal/repository"
  9. v1 "k8s.io/api/core/v1"
  10. "k8s.io/api/extensions/v1beta1"
  11. "k8s.io/client-go/kubernetes"
  12. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  13. "k8s.io/apimachinery/pkg/util/intstr"
  14. )
  15. // GetNGINXIngressServiceIP retrieves the external address of the nginx-ingress service
  16. func GetNGINXIngressServiceIP(clientset kubernetes.Interface) (string, bool, error) {
  17. svcList, err := clientset.CoreV1().Services("").List(context.TODO(), metav1.ListOptions{
  18. LabelSelector: "app.kubernetes.io/managed-by=Helm",
  19. })
  20. if err != nil {
  21. return "", false, err
  22. }
  23. var nginxSvc *v1.Service
  24. exists := false
  25. for _, svc := range svcList.Items {
  26. // check that helm chart annotation is correct exists
  27. if chartAnn, found := svc.ObjectMeta.Labels["helm.sh/chart"]; found {
  28. if (strings.Contains(chartAnn, "ingress-nginx") || strings.Contains(chartAnn, "nginx-ingress")) && svc.Spec.Type == v1.ServiceTypeLoadBalancer {
  29. nginxSvc = &svc
  30. exists = true
  31. break
  32. }
  33. }
  34. }
  35. if !exists {
  36. return "", false, nil
  37. }
  38. if ipArr := nginxSvc.Status.LoadBalancer.Ingress; len(ipArr) > 0 {
  39. // first default to ip, then check hostname
  40. if ipArr[0].IP != "" {
  41. return ipArr[0].IP, true, nil
  42. } else if ipArr[0].Hostname != "" {
  43. return ipArr[0].Hostname, true, nil
  44. }
  45. }
  46. return "", false, nil
  47. }
  48. // DNSRecord wraps the gorm DNSRecord model
  49. type DNSRecord models.DNSRecord
  50. type CreateDNSRecordConfig struct {
  51. ReleaseName string
  52. RootDomain string
  53. Endpoint string
  54. }
  55. // NewDNSRecordForEndpoint generates a random subdomain and returns a DNSRecord
  56. // model
  57. func (c *CreateDNSRecordConfig) NewDNSRecordForEndpoint() *models.DNSRecord {
  58. suffix, _ := repository.GenerateRandomBytes(8)
  59. subdomain := fmt.Sprintf("%s-%s", c.ReleaseName, suffix)
  60. return &models.DNSRecord{
  61. SubdomainPrefix: subdomain,
  62. RootDomain: c.RootDomain,
  63. Endpoint: c.Endpoint,
  64. Hostname: fmt.Sprintf("%s.%s", subdomain, c.RootDomain),
  65. }
  66. }
  67. func (e *DNSRecord) CreateDomain(clientset kubernetes.Interface) error {
  68. // determine if IP address or domain
  69. err := e.createIngress(clientset)
  70. if err != nil {
  71. return err
  72. }
  73. return e.createServiceWithEndpoint(clientset)
  74. }
  75. func (e *DNSRecord) createIngress(clientset kubernetes.Interface) error {
  76. _, err := clientset.ExtensionsV1beta1().Ingresses("default").Create(
  77. context.TODO(),
  78. &v1beta1.Ingress{
  79. ObjectMeta: metav1.ObjectMeta{
  80. Annotations: map[string]string{
  81. "kubernetes.io/ingress.class": "nginx",
  82. "nginx.ingress.kubernetes.io/ssl-redirect": "true",
  83. "nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
  84. "nginx.ingress.kubernetes.io/upstream-vhost": e.Hostname,
  85. },
  86. Name: e.SubdomainPrefix,
  87. Namespace: "default",
  88. },
  89. Spec: v1beta1.IngressSpec{
  90. TLS: []v1beta1.IngressTLS{
  91. {
  92. Hosts: []string{fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain)},
  93. SecretName: "wildcard-cert-tls",
  94. },
  95. },
  96. Rules: []v1beta1.IngressRule{
  97. {
  98. Host: fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain),
  99. IngressRuleValue: v1beta1.IngressRuleValue{
  100. HTTP: &v1beta1.HTTPIngressRuleValue{
  101. Paths: []v1beta1.HTTPIngressPath{
  102. {
  103. Backend: v1beta1.IngressBackend{
  104. ServiceName: e.SubdomainPrefix,
  105. ServicePort: intstr.IntOrString{
  106. Type: intstr.Int,
  107. IntVal: 443,
  108. },
  109. },
  110. },
  111. },
  112. },
  113. },
  114. },
  115. },
  116. },
  117. },
  118. metav1.CreateOptions{},
  119. )
  120. return err
  121. }
  122. func (e *DNSRecord) createServiceWithEndpoint(clientset kubernetes.Interface) error {
  123. // determine if endpoint needs to be created or external name is ok
  124. isIPv4 := net.ParseIP(e.Endpoint) != nil
  125. svcSpec := v1.ServiceSpec{
  126. Ports: []v1.ServicePort{
  127. {
  128. Port: 80,
  129. TargetPort: intstr.IntOrString{
  130. Type: intstr.Int,
  131. IntVal: 80,
  132. },
  133. Name: "http",
  134. },
  135. {
  136. Port: 443,
  137. TargetPort: intstr.IntOrString{
  138. Type: intstr.Int,
  139. IntVal: 443,
  140. },
  141. Name: "https",
  142. },
  143. },
  144. }
  145. // case service spec on ipv4
  146. if isIPv4 {
  147. svcSpec.ClusterIP = "None"
  148. } else {
  149. svcSpec.Type = "ExternalName"
  150. svcSpec.ExternalName = e.Endpoint
  151. }
  152. // create service
  153. _, err := clientset.CoreV1().Services("default").Create(
  154. context.TODO(),
  155. &v1.Service{
  156. ObjectMeta: metav1.ObjectMeta{
  157. Name: e.SubdomainPrefix,
  158. Namespace: "default",
  159. },
  160. Spec: svcSpec,
  161. },
  162. metav1.CreateOptions{},
  163. )
  164. if err != nil {
  165. return err
  166. }
  167. if isIPv4 {
  168. _, err = clientset.CoreV1().Endpoints("default").Create(
  169. context.TODO(),
  170. &v1.Endpoints{
  171. ObjectMeta: metav1.ObjectMeta{
  172. Name: e.SubdomainPrefix,
  173. Namespace: "default",
  174. },
  175. Subsets: []v1.EndpointSubset{
  176. {
  177. Addresses: []v1.EndpointAddress{
  178. {
  179. IP: e.Endpoint,
  180. },
  181. },
  182. Ports: []v1.EndpointPort{
  183. {
  184. Name: "http",
  185. Port: 80,
  186. },
  187. {
  188. Name: "https",
  189. Port: 443,
  190. },
  191. },
  192. },
  193. },
  194. },
  195. metav1.CreateOptions{},
  196. )
  197. }
  198. return err
  199. }