@@ -10,9 +10,16 @@ import (
1010 "time"
1111
1212 "github.com/openrundev/openrun/internal/types"
13+ apps "k8s.io/api/apps/v1"
14+ core "k8s.io/api/core/v1"
15+ apierrors "k8s.io/apimachinery/pkg/api/errors"
16+ meta "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
18+ "k8s.io/apimachinery/pkg/util/intstr"
1319 "k8s.io/client-go/kubernetes"
1420 "k8s.io/client-go/rest"
1521 "k8s.io/client-go/tools/clientcmd"
22+ "k8s.io/client-go/util/retry"
1623)
1724
1825type KubernetesContainerManager struct {
@@ -22,6 +29,11 @@ type KubernetesContainerManager struct {
2229 restConfig * rest.Config
2330}
2431
32+ func sanitizeContainerName (name string ) string {
33+ name = sanitizeName (name )
34+ return name [:60 ] // max length for a Kubernetes object name is 63
35+ }
36+
2537func NewKubernetesContainerManager (logger * types.Logger , config * types.ServerConfig ) (* KubernetesContainerManager , error ) {
2638 cfg , err := loadConfig ()
2739 if err != nil {
@@ -108,30 +120,82 @@ func (k *KubernetesContainerManager) BuildImage(ctx context.Context, imgName Ima
108120}
109121
110122func (k * KubernetesContainerManager ) GetContainerState (ctx context.Context , name ContainerName ) (string , bool , error ) {
111- return "" , false , nil
123+ name = ContainerName (sanitizeContainerName (string (name )))
124+ svc , err := k .clientSet .CoreV1 ().
125+ Services (k .config .Kubernetes .Namespace ).
126+ Get (ctx , string (name ), meta.GetOptions {})
127+ if err != nil {
128+ if apierrors .IsNotFound (err ) {
129+ return "" , false , nil
130+ }
131+ return "" , false , fmt .Errorf ("get service %s/%s: %w" , k .config .Kubernetes .Namespace , string (name ), err )
132+ }
133+ if len (svc .Spec .Ports ) == 0 {
134+ return "" , false , fmt .Errorf ("service %s/%s has no ports" , k .config .Kubernetes .Namespace , string (name ))
135+ }
136+
137+ svcPort := svc .Spec .Ports [0 ].Port
138+ hostNamePort := fmt .Sprintf ("%s.%s.svc.cluster.local:%d" , svc .Name , svc .Namespace , svcPort )
139+
140+ // --- Get Deployment & ready pods ---
141+ dep , err := k .clientSet .AppsV1 ().
142+ Deployments (k .config .Kubernetes .Namespace ).
143+ Get (ctx , string (name ), meta.GetOptions {})
144+ if err != nil {
145+ return "" , false , fmt .Errorf ("get deployment %s/%s: %w" , k .config .Kubernetes .Namespace , string (name ), err )
146+ }
147+
148+ return hostNamePort , dep .Status .ReadyReplicas > 0 , nil
112149}
113150
114151func (k * KubernetesContainerManager ) SupportsInPlaceContainerUpdate () bool {
115- return true
152+ return false
116153}
117154
118155func (k * KubernetesContainerManager ) InPlaceContainerUpdate (ctx context.Context , appEntry * types.AppEntry , containerName ContainerName ,
119156 imageName ImageName , port int64 , envMap map [string ]string , mountArgs []string ,
120157 containerOptions map [string ]string ) error {
121- return nil
158+ // in place upgrade will make it difficult to do atomic upgrade across multiple apps. Instead create a new
159+ // service/deployment during deployment, that will work similar to the way containers are managed in non-k8s scenario.
160+ return fmt .Errorf ("in place container update is not supported for kubernetes container manager" )
122161}
123162
124163func (k * KubernetesContainerManager ) StartContainer (ctx context.Context , name ContainerName ) error {
125- return nil
164+ name = ContainerName (sanitizeContainerName (string (name )))
165+ return retry .RetryOnConflict (retry .DefaultRetry , func () error {
166+ scale , err := k .clientSet .AppsV1 ().Deployments (k .config .Kubernetes .Namespace ).GetScale (ctx , string (name ), meta.GetOptions {})
167+ if err != nil {
168+ return err
169+ }
170+ scale .Spec .Replicas = 1
171+ _ , err = k .clientSet .AppsV1 ().Deployments (k .config .Kubernetes .Namespace ).UpdateScale (ctx , string (name ), scale , meta.UpdateOptions {})
172+ return err
173+ })
126174}
127175
128176func (k * KubernetesContainerManager ) StopContainer (ctx context.Context , name ContainerName ) error {
129- return nil
177+ name = ContainerName (sanitizeContainerName (string (name )))
178+ return retry .RetryOnConflict (retry .DefaultRetry , func () error {
179+ scale , err := k .clientSet .AppsV1 ().Deployments (k .config .Kubernetes .Namespace ).GetScale (ctx , string (name ), meta.GetOptions {})
180+ if err != nil {
181+ return err
182+ }
183+ scale .Spec .Replicas = 0 // scale down to zero
184+ _ , err = k .clientSet .AppsV1 ().Deployments (k .config .Kubernetes .Namespace ).UpdateScale (ctx , string (name ), scale , meta.UpdateOptions {})
185+ return err
186+ })
130187}
131188
132189func (k * KubernetesContainerManager ) RunContainer (ctx context.Context , appEntry * types.AppEntry , containerName ContainerName ,
133190 imageName ImageName , port int64 , envMap map [string ]string , mountArgs []string ,
134191 containerOptions map [string ]string ) error {
192+ imageName = ImageName (k .config .Registry .URL + "/" + string (imageName ))
193+ containerName = ContainerName (sanitizeContainerName (string (containerName )))
194+ hostNamePort , err := k .createDeployment (ctx , string (containerName ), string (imageName ), int32 (port ))
195+ if err != nil {
196+ return fmt .Errorf ("create app: %w" , err )
197+ }
198+ k .Logger .Info ().Msgf ("created app service %s with host name port %s" , containerName , hostNamePort )
135199 return nil
136200}
137201
@@ -146,3 +210,81 @@ func (k *KubernetesContainerManager) VolumeExists(ctx context.Context, name Volu
146210func (k * KubernetesContainerManager ) VolumeCreate (ctx context.Context , name VolumeName ) error {
147211 return nil
148212}
213+
214+ // createDeployment creates a Deployment + Service and returns the Service URL.
215+ func (k * KubernetesContainerManager ) createDeployment (ctx context.Context , name , image string , port int32 ) (string , error ) {
216+ labels := map [string ]string {"app" : name }
217+ replicas := int32 (1 ) // min = max = 1
218+
219+ dep := & apps.Deployment {
220+ ObjectMeta : meta.ObjectMeta {
221+ Name : name ,
222+ Namespace : k .config .Kubernetes .Namespace ,
223+ Labels : labels ,
224+ },
225+ Spec : apps.DeploymentSpec {
226+ Replicas : & replicas ,
227+ Selector : & meta.LabelSelector {
228+ MatchLabels : labels ,
229+ },
230+ Template : core.PodTemplateSpec {
231+ ObjectMeta : meta.ObjectMeta {
232+ Labels : labels ,
233+ },
234+ Spec : core.PodSpec {
235+ Containers : []core.Container {
236+ {
237+ Name : name ,
238+ Image : image ,
239+ Ports : []core.ContainerPort {
240+ {
241+ ContainerPort : port ,
242+ Protocol : core .ProtocolTCP ,
243+ },
244+ },
245+ },
246+ },
247+ },
248+ },
249+ },
250+ }
251+
252+ if _ , err := k .clientSet .AppsV1 ().Deployments (k .config .Kubernetes .Namespace ).Create (ctx , dep , meta.CreateOptions {}); err != nil {
253+ return "" , fmt .Errorf ("create deployment: %w" , err )
254+ }
255+
256+ svc := & core.Service {
257+ ObjectMeta : meta.ObjectMeta {
258+ Name : name ,
259+ Namespace : k .config .Kubernetes .Namespace ,
260+ Labels : labels ,
261+ },
262+ Spec : core.ServiceSpec {
263+ Type : core .ServiceTypeClusterIP ,
264+ Selector : labels ,
265+ Ports : []core.ServicePort {
266+ {
267+ Name : "http" ,
268+ Port : port ,
269+ TargetPort : intstr .FromInt (int (port )),
270+ Protocol : core .ProtocolTCP ,
271+ },
272+ },
273+ },
274+ }
275+
276+ svc , err := k .clientSet .CoreV1 ().Services (k .config .Kubernetes .Namespace ).Create (ctx , svc , meta.CreateOptions {})
277+ if err != nil {
278+ return "" , fmt .Errorf ("create service: %w" , err )
279+ }
280+
281+ if len (svc .Spec .Ports ) == 0 {
282+ return "" , fmt .Errorf ("service has no ports" )
283+ }
284+
285+ // In-cluster DNS URL
286+ servicePort := svc .Spec .Ports [0 ].Port
287+ url := fmt .Sprintf ("%s.%s.svc.cluster.local:%d" , svc .Name , svc .Namespace , servicePort )
288+
289+ return url , nil
290+ }
0 commit comments