Skip to content

Commit 1949e9d

Browse files
committed
proper patch support for user attribute patch api with nested structure
1 parent 596aba4 commit 1949e9d

File tree

5 files changed

+348
-52
lines changed

5 files changed

+348
-52
lines changed

env_gen.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

env_gen.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,10 @@
290290
| DASHBOARD_PORT | string |3000 | Port for dashboard micro-service | | false |
291291
| DEX_HOST | string |http://localhost | | | false |
292292
| DEX_PORT | string |5556 | | | false |
293-
| GIT_SENSOR_PROTOCOL | string |GRPC | Protocol to connect with git-sensor micro-service | | false |
293+
| GIT_SENSOR_PROTOCOL | string |REST | Protocol to connect with git-sensor micro-service | | false |
294294
| GIT_SENSOR_SERVICE_CONFIG | string |{"loadBalancingPolicy":"pick_first"} | git-sensor grpc service config | | false |
295295
| GIT_SENSOR_TIMEOUT | int |0 | Timeout for getting response from the git-sensor | | false |
296-
| GIT_SENSOR_URL | string |127.0.0.1:7071 | git-sensor micro-service url | | false |
296+
| GIT_SENSOR_URL | string |127.0.0.1:7070 | git-sensor micro-service url | | false |
297297
| HELM_CLIENT_URL | string |127.0.0.1:50051 | Kubelink micro-service url | | false |
298298
| KUBELINK_GRPC_MAX_RECEIVE_MSG_SIZE | int |20 | | | false |
299299
| KUBELINK_GRPC_MAX_SEND_MSG_SIZE | int |4 | | | false |

pkg/attributes/UserAttributesService.go

Lines changed: 139 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"encoding/json"
2121
"errors"
2222
"github.com/devtron-labs/devtron/internal/sql/repository"
23+
"github.com/devtron-labs/devtron/pkg/attributes/bean"
2324
"github.com/go-pg/pg"
2425
"go.uber.org/zap"
2526
"reflect"
@@ -99,92 +100,181 @@ func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *UserAttribut
99100
}
100101

101102
func (impl UserAttributesServiceImpl) PatchUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) {
102-
userAttribute, err := impl.GetUserAttribute(request)
103+
existingAttribute, err := impl.GetUserAttribute(request)
103104
if err != nil {
104105
impl.logger.Errorw("error while getting user attributes during patch request", "req", request, "error", err)
105-
return nil, errors.New("error occurred while updating user attributes")
106+
return nil, errors.New("error occurred while getting user attributes")
106107
}
107-
if userAttribute == nil {
108+
109+
if existingAttribute == nil {
108110
impl.logger.Info("no data found for request, so going to add instead of update", "req", request)
109-
attributes, err := impl.AddUserAttributes(request)
111+
newAttribute, err := impl.AddUserAttributes(request)
110112
if err != nil {
111113
impl.logger.Errorw("error in adding new user attributes", "req", request, "error", err)
112-
return nil, errors.New("error occurred while updating user attributes")
114+
return nil, errors.New("error occurred while adding user attributes")
113115
}
114-
return attributes, nil
116+
return newAttribute, nil
115117
}
116118

117-
// Parse existing JSON
118-
var existingData map[string]interface{}
119-
if userAttribute.Value != "" {
120-
err = json.Unmarshal([]byte(userAttribute.Value), &existingData)
121-
if err != nil {
122-
impl.logger.Errorw("error parsing existing json value", "value", userAttribute.Value, "error", err)
123-
return nil, errors.New("error occurred while updating user attributes")
124-
}
125-
} else {
126-
existingData = make(map[string]interface{})
119+
existingData, err := impl.parseJSONValue(existingAttribute.Value, "existing")
120+
if err != nil {
121+
return nil, err
127122
}
128123

129-
// Parse new JSON
130-
var newData map[string]interface{}
131-
if request.Value != "" {
132-
err = json.Unmarshal([]byte(request.Value), &newData)
133-
if err != nil {
134-
impl.logger.Errorw("error parsing request json value", "value", request.Value, "error", err)
135-
return nil, errors.New("error occurred while updating user attributes")
136-
}
137-
} else {
138-
newData = make(map[string]interface{})
124+
newData, err := impl.parseJSONValue(request.Value, "request")
125+
if err != nil {
126+
return nil, err
139127
}
140128

141-
// Check if there are any changes
142-
anyChanges := false
129+
// Merge the data
130+
hasChanges := impl.mergeUserAttributesData(existingData, newData)
131+
if !hasChanges {
132+
impl.logger.Infow("no changes detected, skipping update", "key", request.Key)
133+
return existingAttribute, nil
134+
}
143135

144-
// Merge the objects (patch style)
145-
for key, newValue := range newData {
146-
existingValue, exists := existingData[key]
147-
if !exists || !reflect.DeepEqual(existingValue, newValue) {
148-
existingData[key] = newValue
149-
anyChanges = true
150-
}
136+
// Update in database and return result
137+
return impl.updateAttributeInDatabase(request, existingData)
138+
}
139+
140+
// parseJSONValue parses a JSON string into a map, with proper error handling
141+
func (impl UserAttributesServiceImpl) parseJSONValue(jsonValue, context string) (map[string]interface{}, error) {
142+
var data map[string]interface{}
143+
144+
if jsonValue == "" {
145+
return make(map[string]interface{}), nil
151146
}
152147

153-
// If no changes, return the existing data
154-
if !anyChanges {
155-
impl.logger.Infow("no change detected, skipping update", "key", request.Key)
156-
return userAttribute, nil
148+
err := json.Unmarshal([]byte(jsonValue), &data)
149+
if err != nil {
150+
impl.logger.Errorw("error parsing JSON value", "context", context, "value", jsonValue, "error", err)
151+
return nil, errors.New("error occurred while parsing user attributes data")
157152
}
158153

159-
// Convert back to JSON string
160-
mergedJson, err := json.Marshal(existingData)
154+
return data, nil
155+
}
156+
157+
// updateAttributeInDatabase updates the merged data in the database
158+
func (impl UserAttributesServiceImpl) updateAttributeInDatabase(request *UserAttributesDto, mergedData map[string]interface{}) (*UserAttributesDto, error) {
159+
// Convert merged data back to JSON
160+
mergedJSON, err := json.Marshal(mergedData)
161161
if err != nil {
162-
impl.logger.Errorw("error converting merged data to json", "data", existingData, "error", err)
163-
return nil, errors.New("error occurred while updating user attributes")
162+
impl.logger.Errorw("error converting merged data to JSON", "data", mergedData, "error", err)
163+
return nil, errors.New("error occurred while processing user attributes")
164164
}
165165

166+
// Create DAO for database update
166167
dao := &repository.UserAttributesDao{
167168
EmailId: request.EmailId,
168169
Key: request.Key,
169-
Value: string(mergedJson),
170+
Value: string(mergedJSON),
170171
UserId: request.UserId,
171172
}
172173

174+
// Update in database
173175
err = impl.attributesRepository.UpdateDataValByKey(dao)
174176
if err != nil {
175-
impl.logger.Errorw("error in update attributes", "req", dao, "error", err)
177+
impl.logger.Errorw("error updating user attributes in database", "dao", dao, "error", err)
176178
return nil, errors.New("error occurred while updating user attributes")
177179
}
178180

179-
// Return the updated data
180-
result := &UserAttributesDto{
181+
// Build and return response
182+
return impl.buildResponseDTO(request, string(mergedJSON)), nil
183+
}
184+
185+
// buildResponseDTO creates the response DTO
186+
func (impl UserAttributesServiceImpl) buildResponseDTO(request *UserAttributesDto, mergedValue string) *UserAttributesDto {
187+
return &UserAttributesDto{
181188
EmailId: request.EmailId,
182189
Key: request.Key,
183-
Value: string(mergedJson),
190+
Value: mergedValue,
184191
UserId: request.UserId,
185192
}
193+
}
194+
195+
// mergeUserAttributesData merges newData into existingData with special handling for resources
196+
func (impl UserAttributesServiceImpl) mergeUserAttributesData(existingData, newData map[string]interface{}) bool {
197+
hasChanges := false
198+
199+
for key, newValue := range newData {
200+
if key == bean.UserPreferencesResourcesKey {
201+
// Special handling for resources - merge nested structure
202+
if impl.mergeResourcesData(existingData, newValue) {
203+
hasChanges = true
204+
}
205+
} else {
206+
if impl.mergeStandardAttribute(existingData, key, newValue) {
207+
hasChanges = true
208+
}
209+
}
210+
}
211+
212+
return hasChanges
213+
}
214+
215+
// mergeStandardAttribute merges a standard (non-resource) attribute
216+
func (impl UserAttributesServiceImpl) mergeStandardAttribute(existingData map[string]interface{}, key string, newValue interface{}) bool {
217+
existingValue, exists := existingData[key]
218+
if !exists || !reflect.DeepEqual(existingValue, newValue) {
219+
existingData[key] = newValue
220+
return true
221+
}
222+
return false
223+
}
224+
225+
// mergeResourcesData handles the special merging logic for the resources object
226+
func (impl UserAttributesServiceImpl) mergeResourcesData(existingData map[string]interface{}, newResourcesValue interface{}) bool {
227+
impl.ensureResourcesStructureExists(existingData)
228+
229+
existingResources, ok := existingData[bean.UserPreferencesResourcesKey].(map[string]interface{})
230+
if !ok {
231+
existingData[bean.UserPreferencesResourcesKey] = newResourcesValue
232+
return true
233+
}
234+
235+
newResources, ok := newResourcesValue.(map[string]interface{})
236+
if !ok {
237+
existingData[bean.UserPreferencesResourcesKey] = newResourcesValue
238+
return true
239+
}
186240

187-
return result, nil
241+
return impl.mergeResourceTypes(existingResources, newResources)
242+
}
243+
244+
// ensureResourcesStructureExists initializes the resources structure if it doesn't exist
245+
func (impl UserAttributesServiceImpl) ensureResourcesStructureExists(existingData map[string]interface{}) {
246+
if existingData[bean.UserPreferencesResourcesKey] == nil {
247+
existingData[bean.UserPreferencesResourcesKey] = make(map[string]interface{})
248+
}
249+
}
250+
251+
// mergeResourceTypes merges individual resource types from new resources into existing resources
252+
func (impl UserAttributesServiceImpl) mergeResourceTypes(existingResources, newResources map[string]interface{}) bool {
253+
hasChanges := false
254+
255+
// Merge each resource type from newResources
256+
for resourceType, newResourceData := range newResources {
257+
existingResourceData, exists := existingResources[resourceType]
258+
if !exists || !reflect.DeepEqual(existingResourceData, newResourceData) {
259+
existingResources[resourceType] = newResourceData
260+
hasChanges = true
261+
}
262+
}
263+
264+
return hasChanges
265+
}
266+
267+
// initializeSupportedResourceTypes ensures all supported resource types are initialized
268+
func (impl UserAttributesServiceImpl) initializeSupportedResourceTypes(existingResources map[string]interface{}) {
269+
supportedResourceTypes := []string{"cluster", "job", "app-group", "application/devtron-application"}
270+
271+
for _, resourceType := range supportedResourceTypes {
272+
if existingResources[resourceType] == nil {
273+
existingResources[resourceType] = map[string]interface{}{
274+
"recently-visited": []interface{}{},
275+
}
276+
}
277+
}
188278
}
189279

190280
func (impl UserAttributesServiceImpl) GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error) {

0 commit comments

Comments
 (0)