From bbafe1088eefc1bc3871260d52b00fea35744499 Mon Sep 17 00:00:00 2001 From: James Peru Date: Sat, 20 Jun 2026 13:27:24 +0300 Subject: [PATCH] Scope GetTemplateByName/GetIsoByName by-ID lookup to the zone (#87) GetTemplateByName (and GetIsoByName) resolve the id with the zoneid constraint via GetTemplateID, then call GetTemplateByID/GetIsoByID WITHOUT the zone. A template/ISO registered in multiple zones lists one row per zone for the same UUID, so the helper fails with 'There is more then one result for Template UUID: ...!' (#87). Fix the generator to pass the zone into the by-ID lookup via the existing WithZone option for Template/Iso, and apply it to the two generated helpers. WithZone is a no-op on empty zoneid and uses the id directly for a UUID. Adds a regression test reproducing #87. Signed-off-by: James Peru --- cloudstack/ISOService.go | 2 +- cloudstack/TemplateService.go | 2 +- generate/generate.go | 8 ++- test/GetTemplateByNameZoneRegression_test.go | 66 ++++++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 test/GetTemplateByNameZoneRegression_test.go diff --git a/cloudstack/ISOService.go b/cloudstack/ISOService.go index 5000bc5..d98f004 100644 --- a/cloudstack/ISOService.go +++ b/cloudstack/ISOService.go @@ -2300,7 +2300,7 @@ func (s *ISOService) GetIsoByName(name string, isofilter string, zoneid string, return nil, count, err } - r, count, err := s.GetIsoByID(id, opts...) + r, count, err := s.GetIsoByID(id, append(opts, WithZone(zoneid))...) if err != nil { return nil, count, err } diff --git a/cloudstack/TemplateService.go b/cloudstack/TemplateService.go index e9f9071..dd7f68e 100644 --- a/cloudstack/TemplateService.go +++ b/cloudstack/TemplateService.go @@ -2844,7 +2844,7 @@ func (s *TemplateService) GetTemplateByName(name string, templatefilter string, return nil, count, err } - r, count, err := s.GetTemplateByID(id, templatefilter, opts...) + r, count, err := s.GetTemplateByID(id, templatefilter, append(opts, WithZone(zoneid))...) if err != nil { return nil, count, err } diff --git a/generate/generate.go b/generate/generate.go index 448b614..c11bf76 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -1668,7 +1668,13 @@ func (s *service) generateHelperFuncs(a *API) { p("%s, ", s.parseParamName(ap.Name)) } } - pn("opts...)") + // Constrain the by-ID lookup to the same zone; otherwise a Template/ISO + // registered in multiple zones returns multiple rows for one UUID (#87). + if parseSingular(ln) == "Template" || parseSingular(ln) == "Iso" { + pn("append(opts, WithZone(zoneid))...)") + } else { + pn("opts...)") + } pn(" if err != nil {") pn(" return nil, count, err") pn(" }") diff --git a/test/GetTemplateByNameZoneRegression_test.go b/test/GetTemplateByNameZoneRegression_test.go new file mode 100644 index 0000000..f579f42 --- /dev/null +++ b/test/GetTemplateByNameZoneRegression_test.go @@ -0,0 +1,66 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" +) + +// Regression test for issue #87: GetTemplateByName must carry the zoneid +// constraint into the by-ID lookup. Otherwise a template registered in multiple +// zones lists multiple rows for one UUID and the helper fails with +// "There is more then one result for Template UUID: ...!". +func TestGetTemplateByNameScopesByIDLookupToZone(t *testing.T) { + const tmplID = "06145677-058a-456a-89a0-af4afd6fffcf" + const zoneID = "11111111-2222-3333-4444-555555555555" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + w.Header().Set("Content-Type", "application/json") + if q.Get("id") != "" { + // by-ID lookup: a multi-zone template returns one row PER zone unless + // the zoneid is passed through (the bug). With the fix, zoneid is set. + if q.Get("zoneid") != "" { + fmt.Fprintf(w, `{"listtemplatesresponse":{"count":1,"template":[{"id":%q,"name":"mytmpl","zoneid":"zoneA"}]}}`, tmplID) + } else { + fmt.Fprintf(w, `{"listtemplatesresponse":{"count":2,"template":[{"id":%q,"name":"mytmpl","zoneid":"zoneA"},{"id":%q,"name":"mytmpl","zoneid":"zoneB"}]}}`, tmplID, tmplID) + } + return + } + // by-name lookup (GetTemplateID) resolves to a single template id + fmt.Fprintf(w, `{"listtemplatesresponse":{"count":1,"template":[{"id":%q,"name":"mytmpl"}]}}`, tmplID) + })) + defer server.Close() + + client := cloudstack.NewClient(server.URL, "APIKEY", "SECRETKEY", true) + + tmpl, _, err := client.Template.GetTemplateByName("mytmpl", "all", zoneID) + if err != nil { + t.Fatalf("expected zone-scoped GetTemplateByName to succeed, got error: %v", err) + } + if tmpl == nil || tmpl.Id != tmplID { + t.Fatalf("expected template %s, got %+v", tmplID, tmpl) + } +}