Skip to content

Commit

Permalink
[k8s plugin] Prepare for implementing livestate apis (#5510)
Browse files Browse the repository at this point in the history
* Add GetLiveResources function to retrieve live resources for an application

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>

* Refactor K8s sync stage to use GetLiveResources for improved resource retrieval

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>

* Add BuildApplicationLiveState and helper functions for application live state management

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>

* Add comments

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>

* Add tests for BuildApplicationLiveState

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>

* Fix imports

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>

---------

Signed-off-by: Shinnosuke Sawada-Dazai <[email protected]>
  • Loading branch information
Warashi authored Jan 31, 2025
1 parent e600dd9 commit c803e2c
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 16 deletions.
18 changes: 2 additions & 16 deletions pkg/app/pipedv1/plugin/kubernetes/deployment/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"cmp"
"context"
"errors"
"fmt"
"time"

kubeconfig "github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes/config"
Expand Down Expand Up @@ -118,22 +117,9 @@ func (a *DeploymentService) executeK8sSyncStage(ctx context.Context, lp logpersi

lp.Info("Start finding all running resources but no longer defined in Git")

namespacedLiveResources, err := kubectl.GetAll(ctx, deployTargetConfig.KubeConfigPath,
"",
fmt.Sprintf("%s=%s", provider.LabelManagedBy, provider.ManagedByPiped),
fmt.Sprintf("%s=%s", provider.LabelApplication, input.GetDeployment().GetApplicationId()),
)
if err != nil {
lp.Errorf("Failed while listing all resources (%v)", err)
return model.StageStatus_STAGE_FAILURE
}

clusterScopedLiveResources, err := kubectl.GetAllClusterScoped(ctx, deployTargetConfig.KubeConfigPath,
fmt.Sprintf("%s=%s", provider.LabelManagedBy, provider.ManagedByPiped),
fmt.Sprintf("%s=%s", provider.LabelApplication, input.GetDeployment().GetApplicationId()),
)
namespacedLiveResources, clusterScopedLiveResources, err := provider.GetLiveResources(ctx, kubectl, deployTargetConfig.KubeConfigPath, input.GetDeployment().GetApplicationId())
if err != nil {
lp.Errorf("Failed while listing all cluster-scoped resources (%v)", err)
lp.Errorf("Failed while getting live resources (%v)", err)
return model.StageStatus_STAGE_FAILURE
}

Expand Down
91 changes: 91 additions & 0 deletions pkg/app/pipedv1/plugin/kubernetes/provider/liveresources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2024 The PipeCD Authors.
//
// 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 provider

import (
"context"
"fmt"
"time"

"github.com/pipe-cd/pipecd/pkg/model"
)

// GetLiveResources returns all live resources that belong to the given application.
func GetLiveResources(ctx context.Context, kubectl *Kubectl, kubeconfig string, appID string, selector ...string) (namespaceScoped []Manifest, clusterScoped []Manifest, _ error) {
namespacedLiveResources, err := kubectl.GetAll(ctx, kubeconfig,
"",
fmt.Sprintf("%s=%s", LabelManagedBy, ManagedByPiped),
fmt.Sprintf("%s=%s", LabelApplication, appID),
)
if err != nil {
return nil, nil, fmt.Errorf("failed while listing all namespace-scoped resources (%v)", err)
}

clusterScopedLiveResources, err := kubectl.GetAllClusterScoped(ctx, kubeconfig,
fmt.Sprintf("%s=%s", LabelManagedBy, ManagedByPiped),
fmt.Sprintf("%s=%s", LabelApplication, appID),
)
if err != nil {
return nil, nil, fmt.Errorf("failed while listing all cluster-scoped resources (%v)", err)
}

return namespacedLiveResources, clusterScopedLiveResources, nil
}

// BuildApplicationLiveState builds the live state of the application from the given manifests.
func BuildApplicationLiveState(deploytarget string, manifests []Manifest, now time.Time) *model.ApplicationLiveState {
if len(manifests) == 0 {
return &model.ApplicationLiveState{
HealthStatus: model.ApplicationLiveState_UNKNOWN,
}
}

states := make([]*model.ResourceState, 0, len(manifests))
for _, m := range manifests {
states = append(states, buildResourceState(m, now))
}

return &model.ApplicationLiveState{
Resources: states,
HealthStatus: model.ApplicationLiveState_UNKNOWN, // TODO: Implement health status calculation
}
}

// buildResourceState builds the resource state from the given manifest.
func buildResourceState(m Manifest, now time.Time) *model.ResourceState {
var parents []string // default as nil
if len(m.body.GetOwnerReferences()) > 0 {
parents = make([]string, 0, len(m.body.GetOwnerReferences()))
for _, o := range m.body.GetOwnerReferences() {
parents = append(parents, string(o.UID))
}
}

return &model.ResourceState{
Id: string(m.body.GetUID()),
Name: m.body.GetName(),
ParentIds: parents,
HealthStatus: model.ResourceState_UNKNOWN, // TODO: Implement health status calculation
HealthDescription: "", // TODO: Implement health status calculation
ResourceType: m.body.GetKind(),
ResourceMetadata: map[string]string{
"Namespace": m.body.GetNamespace(),
"API Version": m.body.GetAPIVersion(),
"Kind": m.body.GetKind(),
},
CreatedAt: m.body.GetCreationTimestamp().Unix(),
UpdatedAt: now.Unix(),
}
}
195 changes: 195 additions & 0 deletions pkg/app/pipedv1/plugin/kubernetes/provider/liveresources_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2024 The PipeCD Authors.
//
// 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 provider

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/pipe-cd/pipecd/pkg/model"
)

func TestBuildApplicationLiveState(t *testing.T) {
now := time.Now()

tests := []struct {
name string
manifests []Manifest
want *model.ApplicationLiveState
}{
{
name: "single pod",
manifests: []Manifest{
{
body: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
"namespace": "default",
"uid": "test-uid",
"creationTimestamp": now.Format(time.RFC3339),
},
},
},
},
},
want: &model.ApplicationLiveState{
Resources: []*model.ResourceState{
{
Id: "test-uid",
Name: "test-pod",
ResourceType: "Pod",
ResourceMetadata: map[string]string{
"Namespace": "default",
"API Version": "v1",
"Kind": "Pod",
},
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
},
},
HealthStatus: model.ApplicationLiveState_UNKNOWN,
},
},
{
name: "single pod with owner references",
manifests: []Manifest{
{
body: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
"namespace": "default",
"uid": "test-uid",
"creationTimestamp": now.Format(time.RFC3339),
"ownerReferences": []interface{}{
map[string]interface{}{
"uid": "owner-uid",
},
},
},
},
},
},
},
want: &model.ApplicationLiveState{
Resources: []*model.ResourceState{
{
Id: "test-uid",
Name: "test-pod",
ResourceType: "Pod",
ResourceMetadata: map[string]string{
"Namespace": "default",
"API Version": "v1",
"Kind": "Pod",
},
ParentIds: []string{"owner-uid"},
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
},
},
HealthStatus: model.ApplicationLiveState_UNKNOWN,
},
},
{
name: "multiple resources with owner references",
manifests: []Manifest{
{
body: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod-1",
"namespace": "default",
"uid": "test-uid-1",
"creationTimestamp": now.Format(time.RFC3339),
"ownerReferences": []interface{}{
map[string]interface{}{
"uid": "owner-uid-1",
},
},
},
},
},
},
{
body: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "test-service",
"namespace": "default",
"uid": "test-uid-2",
"creationTimestamp": now.Format(time.RFC3339),
"ownerReferences": []interface{}{
map[string]interface{}{
"uid": "owner-uid-2",
},
},
},
},
},
},
},
want: &model.ApplicationLiveState{
Resources: []*model.ResourceState{
{
Id: "test-uid-1",
Name: "test-pod-1",
ResourceType: "Pod",
ResourceMetadata: map[string]string{
"Namespace": "default",
"API Version": "v1",
"Kind": "Pod",
},
ParentIds: []string{"owner-uid-1"},
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
},
{
Id: "test-uid-2",
Name: "test-service",
ResourceType: "Service",
ResourceMetadata: map[string]string{
"Namespace": "default",
"API Version": "v1",
"Kind": "Service",
},
ParentIds: []string{"owner-uid-2"},
CreatedAt: now.Unix(),
UpdatedAt: now.Unix(),
},
},
HealthStatus: model.ApplicationLiveState_UNKNOWN,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := BuildApplicationLiveState("test-deploytarget", tt.manifests, now)
assert.Equal(t, tt.want, got, "expected live state to be equal to the expected one")
})
}
}

0 comments on commit c803e2c

Please sign in to comment.