1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package resourceskeleton
16
17 import (
18 "context"
19 "fmt"
20 "net/url"
21 "strings"
22
23 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
24 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/asset"
25 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/serviceclient"
26 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration"
27 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
28 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
29 uri2 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceskeleton/uri"
30 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
31
32 tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
33 "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
35 "k8s.io/apimachinery/pkg/runtime/schema"
36 )
37
38 const ProjectKind = "Project"
39
40 var ResourceManagerAPIGroupName = fmt.Sprintf("resourcemanager.%v", crdgeneration.ApiDomain)
41
42 func NewProject(projectId string, smLoader *servicemappingloader.ServiceMappingLoader) (*unstructured.Unstructured, error) {
43 sm, err := smLoader.GetServiceMapping(ResourceManagerAPIGroupName)
44 if err != nil {
45 return nil, fmt.Errorf("error getting service mapping for '%v': %v", ResourceManagerAPIGroupName, err)
46 }
47 u := &unstructured.Unstructured{}
48 gvk := schema.GroupVersionKind{
49 Group: sm.Name,
50 Version: sm.Spec.Version,
51 Kind: ProjectKind,
52 }
53 u.SetGroupVersionKind(gvk)
54 u.SetName(projectId)
55 annotations := make(map[string]string, 1)
56 annotations[k8s.FolderIDAnnotation] = "skeleton-folder"
57 u.SetAnnotations(annotations)
58 return u, nil
59 }
60
61 func NewFromURI(uri string, smLoader *servicemappingloader.ServiceMappingLoader, tfProvider *tfschema.Provider) (*unstructured.Unstructured, error) {
62 parsedUrl, err := url.Parse(uri)
63 if err != nil {
64 return nil, fmt.Errorf("error parsing '%v' as url: %w", uri, err)
65 }
66 sm, rc, err := uri2.GetServiceMappingAndResourceConfig(smLoader, parsedUrl.Host, parsedUrl.Path)
67 if err != nil {
68 return nil, fmt.Errorf("error getting service mapping and resource config for url '%v': %w", uri, err)
69 }
70 tfInfo := terraform.InstanceInfo{
71 Type: rc.Name,
72 }
73 state, err := krmtotf.ImportState(context.Background(), strings.TrimPrefix(parsedUrl.Path, "/"), &tfInfo, tfProvider)
74 if err != nil {
75 return nil, fmt.Errorf("error importing resource name to TF state: %w", err)
76 }
77 resource, err := tfStateToResource(state, sm, rc, tfProvider)
78 if err != nil {
79 return nil, fmt.Errorf("error creating new resource: %w", err)
80 }
81 return resource.MarshalAsUnstructured()
82 }
83
84 func NewFromAsset(a *asset.Asset, smLoader *servicemappingloader.ServiceMappingLoader, tfProvider *tfschema.Provider, serviceClient serviceclient.ServiceClient) (*unstructured.Unstructured, error) {
85 sm, rc, err := asset.GetServiceMappingAndResourceConfig(smLoader, a)
86 if err != nil {
87 return nil, err
88 }
89 tfInfo := terraform.InstanceInfo{
90 Type: rc.Name,
91 }
92 name := trimServiceHostName(a, sm)
93 importID, err := convertAssetNameToImportID(a, rc, name)
94 if err != nil {
95 return nil, fmt.Errorf("error coverting cloud asset inventory name '%v' to resource id: %v", name, err)
96 }
97 state, err := krmtotf.ImportState(context.Background(), importID, &tfInfo, tfProvider)
98 if err != nil {
99 return nil, fmt.Errorf("error importing resource name to TF state: %v", err)
100 }
101 resource, err := tfStateToResource(state, sm, rc, tfProvider)
102 if err != nil {
103 return nil, fmt.Errorf("error creating new resource: %v", err)
104 }
105 err = applyAssetKRMResourceHacks(resource, a, serviceClient, state)
106 if err != nil {
107 return nil, fmt.Errorf("unable to apply asset KRM hacks on asset %v: %v", a, err)
108 }
109 return resource.MarshalAsUnstructured()
110 }
111
112 func tfStateToResource(state *terraform.InstanceState, sm *v1alpha1.ServiceMapping, rc *v1alpha1.ResourceConfig, tfProvider *tfschema.Provider) (*krmtotf.Resource, error) {
113 resource, err := krmtotf.NewResourceFromResourceConfig(rc, tfProvider)
114 if err != nil {
115 return nil, fmt.Errorf("error creating new resource: %v", err)
116 }
117 gvk := schema.GroupVersionKind{
118 Group: sm.Name,
119 Version: sm.GetVersionFor(rc),
120 Kind: rc.Kind,
121 }
122
123 resource.SetGroupVersionKind(gvk)
124 resource.Spec, resource.Status = krmtotf.GetSpecAndStatusFromState(resource, state)
125 resource.Labels = krmtotf.GetLabelsFromState(resource, state)
126 resource.Annotations = krmtotf.GetAnnotationsFromState(resource, state)
127 resource.Name = krmtotf.GetNameFromState(resource, state)
128 return resource, nil
129 }
130
131 func trimServiceHostName(a *asset.Asset, sm *v1alpha1.ServiceMapping) string {
132 return strings.TrimPrefix(a.Name, fmt.Sprintf("//%v/", sm.Spec.ServiceHostName))
133 }
134
135
136
137 func convertAssetNameToImportID(a *asset.Asset, rc *v1alpha1.ResourceConfig, name string) (string, error) {
138
139 if rc.Kind == "IAMCustomRole" {
140 id, err := parseIAMCustomRoleID(name)
141 if err != nil {
142 return "", fmt.Errorf("unable to parse IAMCustomRole id: %v", err)
143 }
144 switch id.parentType {
145 case Project:
146 return fmt.Sprintf("%v##%v", id.parentID, id.roleID), nil
147 case Organization:
148 return fmt.Sprintf("#%v#%v", id.parentID, id.roleID), nil
149 }
150 }
151 if rc.Kind == "MonitoringAlertPolicy" {
152 partitions := strings.Split(name, "/")
153 if len(partitions) != 4 {
154 return "", fmt.Errorf("expected 4 partitions split by '/' for '%v'", name)
155 }
156 return fmt.Sprintf("%v projects/%v/alertPolicies/%v", partitions[1], partitions[1], partitions[3]), nil
157 }
158
159 return name, nil
160 }
161
162
163 func applyAssetKRMResourceHacks(resource *krmtotf.Resource, a *asset.Asset, client serviceclient.ServiceClient, state *terraform.InstanceState) error {
164 if resource.Kind == "StorageBucket" {
165
166
167
168
169
170 for _, ancestor := range a.Ancestors {
171 if strings.HasPrefix(ancestor, "projects/") {
172 projectNumber := strings.Replace(ancestor, "projects/", "", 1)
173 project, err := client.GetProjectFromProjectIDOrNumber(projectNumber)
174 if err != nil {
175 return err
176 }
177 resource.Annotations[k8s.ProjectIDAnnotation] = project.ProjectId
178 }
179 }
180 } else if resource.Kind == "IAMCustomRole" {
181 id, err := parseIAMCustomRoleID(state.ID)
182 if err != nil {
183 return fmt.Errorf("unable to parse IAMCustomRole id: %v", err)
184 }
185 if resource.Spec == nil {
186 resource.Spec = make(map[string]interface{})
187 }
188 resource.Spec[k8s.ResourceIDFieldName] = id.roleID
189 switch id.parentType {
190 case Project:
191 resource.Annotations[k8s.ProjectIDAnnotation] = id.parentID
192 case Organization:
193 resource.Annotations[k8s.OrgIDAnnotation] = id.parentID
194 }
195 }
196 return nil
197 }
198
199 type parentType int32
200
201 const (
202 Project parentType = iota
203 Organization
204 )
205
206 type iamCustomRoleID struct {
207 parentType parentType
208 parentID string
209 roleID string
210 }
211
212
213
214 func parseIAMCustomRoleID(id string) (*iamCustomRoleID, error) {
215 partitions := strings.Split(id, "/")
216 if len(partitions) != 4 {
217 return nil, fmt.Errorf("expected 4 partitions split by '/' for for '%v'", id)
218 }
219 value := iamCustomRoleID{
220 parentID: partitions[1],
221 roleID: partitions[3],
222 }
223 switch partitions[0] {
224 case "projects":
225 value.parentType = Project
226 case "organizations":
227 value.parentType = Organization
228 default:
229 return nil, fmt.Errorf("expected 'projects' or 'organizations' for first partition, got '%v'", partitions[0])
230 }
231 return &value, nil
232 }
233
View as plain text